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/.envrc b/.envrc index 190b5b2b3..1d11c5354 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ -use flake +use nix watch_file nix/*.nix diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 528b128b1..c7d36db27 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,9 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a + +# reformat codebase +1d468ac35ad88d8c77cc83f25e3704d9bd7df01b + +# format a part of codebase +5c8481a118c8fefbfe901001d7828eaf6866eac4 diff --git a/.github/actions/get-merge-commit/action.yml b/.github/actions/get-merge-commit/action.yml new file mode 100644 index 000000000..534d138e1 --- /dev/null +++ b/.github/actions/get-merge-commit/action.yml @@ -0,0 +1,103 @@ +# This file incorporates work covered by the following copyright and +# permission notice +# +# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 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. + +name: Get merge commit +description: Get a merge commit of a given pull request + +inputs: + repository: + description: Repository containing the pull request + required: false + pull-request-id: + description: ID of a pull request + required: true + +outputs: + merge-commit-sha: + description: Git SHA of a merge commit + value: ${{ steps.query.outputs.merge-commit-sha }} + +runs: + using: composite + + steps: + - name: Wait for GitHub to report merge commit + id: query + shell: bash + env: + GITHUB_REPO: ${{ inputs.repository || github.repository }} + PR_ID: ${{ inputs.pull-request-id }} + # https://github.com/NixOS/nixpkgs/blob/8f77f3600f1ee775b85dc2c72fd842768e486ec9/ci/get-merge-commit.sh + run: | + set -euo pipefail + + log() { + echo "$@" >&2 + } + + # Retry the API query this many times + retryCount=5 + # Start with 5 seconds, but double every retry + retryInterval=5 + + while true; do + log "Checking whether the pull request can be merged" + prInfo=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$GITHUB_REPO/pulls/$PR_ID") + + # Non-open PRs won't have their mergeability computed no matter what + state=$(jq -r .state <<<"$prInfo") + if [[ "$state" != open ]]; then + log "PR is not open anymore" + exit 1 + fi + + mergeable=$(jq -r .mergeable <<<"$prInfo") + if [[ "$mergeable" == "null" ]]; then + if ((retryCount == 0)); then + log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/" + exit 3 + else + ((retryCount -= 1)) || true + + # null indicates that GitHub is still computing whether it's mergeable + # Wait a couple seconds before trying again + log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)" + sleep "$retryInterval" + + ((retryInterval *= 2)) || true + fi + else + break + fi + done + + if [[ "$mergeable" == "true" ]]; then + echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT" + else + echo "# 🚨 The PR has a merge conflict!" >> "$GITHUB_STEP_SUMMARY" + exit 2 + fi diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml new file mode 100644 index 000000000..b71e62592 --- /dev/null +++ b/.github/actions/package/linux/action.yml @@ -0,0 +1,124 @@ +name: Package for Linux +description: Create Linux packages for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + default: Linux + cmake-preset: + description: Base CMake preset previously used for the build + required: true + default: linux + qt-version: + description: Version of Qt to use + required: true + gpg-private-key: + description: Private key for AppImage signing + required: false + gpg-private-key-id: + description: ID for the gpg-private-key, to select the signing key + required: false + +runs: + using: composite + + steps: + - name: Package AppImage + shell: bash + env: + VERSION: ${{ inputs.version }} + BUILD_DIR: build + INSTALL_APPIMAGE_DIR: install-appdir + + GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} + run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr + + mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml + export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated + + export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" + + chmod +x linuxdeploy-*.AppImage + + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + 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" + export LD_LIBRARY_PATH + + chmod +x AppImageUpdate-x86_64.AppImage + cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin + + export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" + + if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then + export SIGN=1 + export SIGN_KEY=${{ inputs.gpg-private-key-id }} + mkdir -p ~/.gnupg/ + echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key + gpg --import ~/.gnupg/private.key + else + echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY + fi + + ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg + + mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-x86_64.AppImage" + + - name: Package portable tarball + shell: bash + env: + BUILD_DIR: build + + CMAKE_PRESET: ${{ inputs.cmake-preset }} + + INSTALL_PORTABLE_DIR: install-portable + run: | + cmake --preset "$CMAKE_PRESET" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full + 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 * + + - name: Upload binary tarball + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher-portable.tar.gz + + - name: Upload AppImage + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage + path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage + + - name: Upload AppImage Zsync + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage.zsync + path: PrismLauncher-Linux-x86_64.AppImage.zsync diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml new file mode 100644 index 000000000..42181953c --- /dev/null +++ b/.github/actions/package/macos/action.yml @@ -0,0 +1,121 @@ +name: Package for macOS +description: Create a macOS package for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + default: macOS + 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 + sparkle-ed25519-key: + description: Private key for signing Sparkle updates + required: false + +runs: + using: composite + + steps: + - name: Fetch codesign certificate + shell: bash + run: | + echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12 + if [ -n '${{ inputs.apple-codesign-id }}' ]; then + security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain + security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.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 + shell: bash + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} + + cd ${{ env.INSTALL_DIR }} + chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" + + if [ -n '${{ inputs.apple-codesign-id }}' ]; then + APPLE_CODESIGN_ID='${{ inputs.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" + + - name: Notarize + shell: bash + env: + INSTALL_DIR: install + run: | + cd ${{ env.INSTALL_DIR }} + + if [ -n '${{ inputs.apple-notarize-password }}' ]; then + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + xcrun notarytool submit ../PrismLauncher.zip \ + --wait --progress \ + --apple-id '${{ inputs.apple-notarize-apple-id }}' \ + --team-id '${{ inputs.apple-notarize-team-id }}' \ + --password '${{ inputs.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 + shell: bash + run: | + if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then + echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem + 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: + - :memo: Sparkle Signature (ed25519): \`$signature\` + EOF + else + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Artifact Information :information_source: + - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) + EOF + fi + + - name: Upload binary tarball + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher.zip diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml new file mode 100644 index 000000000..60b2c75d1 --- /dev/null +++ b/.github/actions/package/windows/action.yml @@ -0,0 +1,143 @@ +name: Package for Windows +description: Create a Windows package for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + msystem: + description: MSYS2 subsystem to use + required: true + default: false + windows-codesign-cert: + description: Certificate for signing Windows builds + required: false + windows-codesign-password: + description: Password for signing Windows builds + required: false + +runs: + using: composite + + steps: + - name: Package (MinGW) + if: ${{ inputs.msystem != '' }} + shell: msys2 {0} + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} + touch ${{ env.INSTALL_DIR }}/manifest.txt + for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (MSVC) + if: ${{ inputs.msystem == '' }} + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} + + 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 + + - name: Fetch codesign certificate + shell: bash # yes, we are not using MSYS2 or PowerShell here + run: | + echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx + + - name: Sign executable + shell: pwsh + env: + INSTALL_DIR: install + run: | + if (Get-Content ./codesign.pfx){ + cd ${{ env.INSTALL_DIR }} + # We ship the exact same executable for portable and non-portable editions, so signing just once is fine + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Package (MinGW, portable) + if: ${{ inputs.msystem != '' }} + shell: msys2 {0} + env: + BUILD_DIR: build + INSTALL_DIR: install + INSTALL_PORTABLE_DIR: install-portable + run: | + cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + + - name: Package (MSVC, portable) + if: ${{ inputs.msystem == '' }} + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + INSTALL_PORTABLE_DIR: install-portable + run: | + cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + + Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (installer) + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + + NSCURL_VERSION: "v24.9.26.122" + NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" + run: | + New-Item -Name NSISPlugins -ItemType Directory + Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip + $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash + if ( $nscurl_hash -ne "${{ env.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" + + - name: Sign installer + shell: pwsh + run: | + if (Get-Content ./codesign.pfx){ + SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Upload binary zip + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} + path: install/** + + - name: Upload portable zip + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} + path: install-portable/** + + - name: Upload installer + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher-Setup.exe diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml new file mode 100644 index 000000000..e97abd1df --- /dev/null +++ b/.github/actions/setup-dependencies/action.yml @@ -0,0 +1,78 @@ +name: Setup Dependencies +description: Install and setup dependencies for building Prism Launcher + +inputs: + build-type: + description: Type for the build + required: true + default: Debug + msystem: + description: MSYS2 subsystem to use + required: false + vcvars-arch: + description: Visual Studio architecture to use + required: false + qt-architecture: + description: Qt architecture + required: false + qt-version: + description: Version of Qt to use + required: true + default: 6.8.1 + +outputs: + build-type: + description: Type of build used + value: ${{ inputs.build-type }} + qt-version: + description: Version of Qt used + value: ${{ inputs.qt-version }} + +runs: + using: composite + + steps: + - name: Setup Linux dependencies + if: ${{ runner.os == 'Linux' }} + uses: ./.github/actions/setup-dependencies/linux + + - name: Setup macOS dependencies + if: ${{ runner.os == 'macOS' }} + uses: ./.github/actions/setup-dependencies/macos + + - name: Setup Windows dependencies + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-dependencies/windows + with: + build-type: ${{ inputs.build-type }} + msystem: ${{ inputs.msystem }} + vcvars-arch: ${{ inputs.vcvars-arch }} + + # TODO(@getchoo): Get this working on MSYS2! + - name: Setup ccache + if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} + uses: hendrikmuhs/ccache-action@v1.2.18 + with: + variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }} + create-symlink: ${{ runner.os != 'Windows' }} + key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }} + + - name: Use ccache on debug builds + if: ${{ inputs.build-type == 'Debug' }} + shell: bash + env: + # Only use sccache on MSVC + CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem == '') && 'sccache' || 'ccache' }} + run: | + echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" + echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" + + - name: Install Qt + if: ${{ inputs.msystem == '' }} + uses: jurplel/install-qt-action@v4 + with: + aqtversion: "==3.1.*" + version: ${{ inputs.qt-version }} + arch: ${{ inputs.qt-architecture }} + modules: qt5compat qtimageformats qtnetworkauth + cache: ${{ inputs.build-type == 'Debug' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml new file mode 100644 index 000000000..dd0d28364 --- /dev/null +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -0,0 +1,26 @@ +name: Setup Linux dependencies + +runs: + using: composite + + steps: + - name: Install host dependencies + shell: bash + run: | + sudo apt-get -y update + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev + + - name: Setup AppImage tooling + shell: bash + run: | + declare -A appimage_deps + appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" + appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" + appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + + for url in "${!appimage_deps[@]}"; do + curl -LO "$url" + sha256sum -c - <<< "${appimage_deps[$url]}" + done + + sudo apt -y install libopengl0 diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml new file mode 100644 index 000000000..dcbb308c2 --- /dev/null +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -0,0 +1,16 @@ +name: Setup macOS dependencies + +runs: + using: composite + + steps: + - name: Install dependencies + shell: bash + run: | + brew update + brew install ninja extra-cmake-modules temurin@17 + + - name: Set JAVA_HOME + shell: bash + run: | + echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml new file mode 100644 index 000000000..78717ddf4 --- /dev/null +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -0,0 +1,66 @@ +name: Setup Windows Dependencies + +inputs: + build-type: + description: Type for the build + required: true + default: Debug + msystem: + description: MSYS2 subsystem to use + required: false + vcvars-arch: + description: Visual Studio architecture to use + required: true + default: amd64 + +runs: + using: composite + + steps: + # NOTE: Installed on MinGW as well for SignTool + - name: Enter VS Developer shell + if: ${{ runner.os == 'Windows' }} + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ inputs.vcvars-arch }} + vsversion: 2022 + + - name: Setup MSYS2 (MinGW-64) + if: ${{ inputs.msystem != '' }} + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ inputs.msystem }} + update: true + install: >- + git + pacboy: >- + toolchain:p + ccache:p + cmake:p + extra-cmake-modules:p + ninja:p + qt6-base:p + qt6-svg:p + qt6-imageformats:p + qt6-5compat:p + qt6-networkauth:p + cmark:p + quazip-qt6:p + + - name: Retrieve ccache cache (MinGW) + if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} + uses: actions/cache@v4.2.3 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-mingw-w64-ccache + + - name: Setup ccache (MinGW) + if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} + shell: msys2 {0} + run: | + ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' + ccache --set-config=max_size='500M' + ccache --set-config=compression=true + ccache -p # Show config diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 4146cddf4..c46f8e192 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v3.1.0 + uses: korthout/backport-action@v3.2.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml new file mode 100644 index 000000000..ecbaf755d --- /dev/null +++ b/.github/workflows/blocked-prs.yml @@ -0,0 +1,255 @@ +name: Blocked/Stacked Pull Requests Automation + +on: + pull_request_target: + types: + - opened + - reopened + - edited + - synchronize + workflow_dispatch: + inputs: + pr_id: + description: Local Pull Request number to work on + required: true + type: number + +jobs: + blocked_status: + name: Check Blocked Status + runs-on: ubuntu-latest + + steps: + - name: Generate token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.PULL_REQUEST_APP_ID }} + private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} + + - name: Setup From Dispatch Event + if: github.event_name == 'workflow_dispatch' + id: dispatch_event_setup + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + PR_NUMBER: ${{ inputs.pr_id }} + run: | + # setup env for the rest of the workflow + OWNER=$(dirname "${{ github.repository }}") + REPO=$(basename "${{ github.repository }}") + PR_JSON=$( + gh api \ + -H "Accept: application/vnd.github.raw+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/pulls/$PR_NUMBER" + ) + echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV" + + - name: Setup Environment + id: env_setup + env: + EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }} + run: | + # setup env for the rest of the workflow + PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"} + { + echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")" + echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")" + echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")" + echo "JOB_DATA=$(jq -c ' + { + "repo": .base.repo.name, + "owner": .base.repo.owner.login, + "repoUrl": .base.repo.html_url, + "prNumber": .number, + "prHeadSha": .head.sha, + "prHeadLabel": .head.label, + "prBody": (.body // ""), + "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) + } + ' <<< "$PR_JSON")" + } >> "$GITHUB_ENV" + + + - name: Find Blocked/Stacked PRs in body + id: pr_ids + run: | + prs=$( + jq -c ' + .prBody as $body + | ( + $body | + reduce ( + . | scan("blocked (?:by|on):? #([0-9]+)") + | map({ + "type": "Blocked on", + "number": ( . | tonumber ) + }) + ) as $i ([]; . + [$i[]]) + ) as $bprs + | ( + $body | + reduce ( + . | scan("stacked on:? #([0-9]+)") + | map({ + "type": "Stacked on", + "number": ( . | tonumber ) + }) + ) as $i ([]; . + [$i[]]) + ) as $sprs + | ($bprs + $sprs) as $prs + | { + "blocking": $prs, + "numBlocking": ( $prs | length), + } + ' <<< "$JOB_DATA" + ) + echo "prs=$prs" >> "$GITHUB_OUTPUT" + + - name: Collect Blocked PR Data + id: blocking_data + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }} + run: | + blocked_pr_data=$( + while read -r pr_data ; do + gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \ + | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \ + ' + . | { + "type": $type, + "number": .number, + "merged": .merged, + "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end), + "labels": (reduce .labels[].name as $l ([]; . + [$l])), + "basePrUrl": .html_url, + "baseRepoName": .head.repo.name, + "baseRepoOwner": .head.repo.owner.login, + "baseRepoUrl": .head.repo.html_url, + "baseSha": .head.sha, + "baseRefName": .head.ref, + } + ' + done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s + ) + { + echo "data=$blocked_pr_data"; + echo "all_merged=$(jq -r 'all(.[] | (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")); .)' <<< "$blocked_pr_data")"; + echo "current_blocking=$(jq -c 'map( + select( + (.type == "Stacked on" and (.merged | not)) or + (.type == "Blocked on" and (.state == "Open")) + ) | .number + )' <<< "$blocked_pr_data" )"; + } >> "$GITHUB_OUTPUT" + + - name: Add 'blocked' Label if Missing + id: label_blocked + if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged) + continue-on-error: true + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER" + + - name: Remove 'blocked' Label if All Dependencies Are Merged + id: unlabel_blocked + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged) + continue-on-error: true + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + run: | + gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER" + + - name: Apply 'blocking' Label to Unmerged Dependencies + id: label_blocking + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 + continue-on-error: true + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }} + run: | + while read -r pr ; do + gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true + done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") + + - name: Apply Blocking PR Status Check + id: blocked_check + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 + continue-on-error: true + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} + run: | + pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA") + # create commit Status, overwrites previous identical context + while read -r pr_data ; do + DESC=$( + jq -r 'if .type == "Stacked on" then + "Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged" + else + "Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed" + end ' <<< "$pr_data" + ) + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ + -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \ + -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ + -f "description=$DESC" \ + -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" + done < <(jq -c '.[]' <<< "$BLOCKING_DATA") + + - name: Context Comment + id: generate-comment + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 + continue-on-error: true + env: + BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} + run: | + COMMENT_PATH="$(pwd)/temp_comment_file.txt" + echo '

PR Dependencies :pushpin:

' > "$COMMENT_PATH" + echo >> "$COMMENT_PATH" + pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA") + while read -r pr_data ; do + base_pr=$(jq -r '.number' <<< "$pr_data") + base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data") + base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") + base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data") + compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label" + status=$(jq -r ' + if .type == "Stacked on" then + if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end + else + if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end + end + ' <<< "$pr_data") + type=$(jq -r '.type' <<< "$pr_data") + echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" + done < <(jq -c '.[]' <<< "$BLOCKING_DATA") + + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: 💬 PR Comment + if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 + continue-on-error: true + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + COMMENT_BODY: ${{ steps.generate-comment.outputs.body }} + run: | + gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \ + --body "$COMMENT_BODY" \ + --create-if-none \ + --edit-last + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1ed0a25a..ac33aba27 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,699 +1,195 @@ name: Build on: + push: + branches-ignore: + - "renovate/**" + paths: + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/build.yml" + pull_request: + paths: + # File types + - "**.cpp" + - "**.h" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/build.yml" workflow_call: inputs: - build_type: - description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) + build-type: + description: Type of build (Debug or Release) type: string default: Debug - is_qt_cached: - description: Enable Qt caching or not + workflow_dispatch: + inputs: + build-type: + description: Type of build (Debug or Release) type: string - default: true - secrets: - SPARKLE_ED25519_KEY: - description: Private key for signing Sparkle updates - required: false - WINDOWS_CODESIGN_CERT: - description: Certificate for signing Windows builds - required: false - WINDOWS_CODESIGN_PASSWORD: - description: Password for signing Windows builds - required: false - 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 - CACHIX_AUTH_TOKEN: - description: Private token for authenticating against Cachix cache - required: false - GPG_PRIVATE_KEY: - description: Private key for AppImage signing - required: false - GPG_PRIVATE_KEY_ID: - description: ID for the GPG_PRIVATE_KEY, to select the signing key - required: false + default: Debug jobs: build: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-20.04 - qt_ver: 5 - qt_host: linux - qt_arch: "" - qt_version: "5.12.8" - qt_modules: "qtnetworkauth" - - - os: ubuntu-20.04 - qt_ver: 6 - qt_host: linux - qt_arch: "" - qt_version: "6.2.4" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - - - os: windows-2022 - name: "Windows-MinGW-w64" - msystem: clang64 - vcvars_arch: "amd64_x86" - - - os: windows-2022 - name: "Windows-MSVC" - msystem: "" - architecture: "x64" - vcvars_arch: "amd64" - qt_ver: 6 - qt_host: windows - qt_arch: "" - qt_version: "6.7.3" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - nscurl_tag: "v24.9.26.122" - nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - - - os: windows-2022 - name: "Windows-MSVC-arm64" - msystem: "" - architecture: "arm64" - vcvars_arch: "amd64_arm64" - qt_ver: 6 - qt_host: windows - qt_arch: "win64_msvc2019_arm64" - qt_version: "6.7.3" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - nscurl_tag: "v24.9.26.122" - nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - - - os: macos-14 - name: macOS - macosx_deployment_target: 11.0 - qt_ver: 6 - qt_host: mac - qt_arch: "" - qt_version: "6.7.3" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - - - os: macos-14 - name: macOS-Legacy - macosx_deployment_target: 10.13 - qt_ver: 5 - qt_host: mac - qt_version: "5.15.2" - qt_modules: "qtnetworkauth" - - runs-on: ${{ matrix.os }} - - env: - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - INSTALL_DIR: "install" - INSTALL_PORTABLE_DIR: "install-portable" - INSTALL_APPIMAGE_DIR: "install-appdir" - BUILD_DIR: "build" - CCACHE_VAR: "" - HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - - steps: - ## - # PREPARE - ## - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: "true" - - - name: "Setup MSYS2" - if: runner.os == 'Windows' && matrix.msystem != '' - uses: msys2/setup-msys2@v2 - with: - msystem: ${{ matrix.msystem }} - update: true - install: >- - git - mingw-w64-x86_64-binutils - pacboy: >- - toolchain:p - cmake:p - extra-cmake-modules:p - ninja:p - qt6-base:p - qt6-svg:p - qt6-imageformats:p - quazip-qt6:p - ccache:p - qt6-5compat:p - qt6-networkauth:p - cmark:p - - - name: Force newer ccache - if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' - run: | - choco install ccache --version 4.7.1 - - - name: Setup ccache - if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.14 - 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@v4.1.1 - with: - path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} - restore-keys: | - ${{ matrix.os }}-mingw-w64-ccache - - - name: Setup ccache (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - shell: msys2 {0} - run: | - ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' - ccache --set-config=max_size='500M' - ccache --set-config=compression=true - ccache -p # Show config - ccache -z # Zero stats - - - name: Use ccache on Debug builds only - if: inputs.build_type == 'Debug' - shell: bash - run: | - echo "CCACHE_VAR=ccache" >> $GITHUB_ENV - - - name: Set short version - shell: bash - run: | - ver_short=`git rev-parse --short HEAD` - echo "VERSION=$ver_short" >> $GITHUB_ENV - - - name: Install Dependencies (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream - - - name: Install Dependencies (macOS) - if: runner.os == 'macOS' - run: | - brew update - brew install ninja extra-cmake-modules - - - name: Install host Qt (Windows MSVC arm64) - if: runner.os == 'Windows' && matrix.architecture == 'arm64' - uses: jurplel/install-qt-action@v3 - with: - aqtversion: "==3.1.*" - py7zrversion: ">=0.20.2" - version: ${{ matrix.qt_version }} - host: "windows" - target: "desktop" - arch: "" - modules: ${{ matrix.qt_modules }} - cache: ${{ inputs.is_qt_cached }} - cache-key-prefix: host-qt-arm64-windows - dir: ${{ github.workspace }}\HostQt - set-env: false - - - name: Install Qt (macOS, Linux & Windows MSVC) - if: matrix.msystem == '' - uses: jurplel/install-qt-action@v3 - with: - aqtversion: "==3.1.*" - py7zrversion: ">=0.20.2" - version: ${{ matrix.qt_version }} - target: "desktop" - arch: ${{ matrix.qt_arch }} - modules: ${{ matrix.qt_modules }} - tools: ${{ matrix.qt_tools }} - cache: ${{ inputs.is_qt_cached }} - - - name: Install MSVC (Windows MSVC) - if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool - uses: ilammy/msvc-dev-cmd@v1 - with: - vsversion: 2022 - arch: ${{ matrix.vcvars_arch }} - - - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 - run: | - wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" - wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" - wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" - - wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" - - sudo apt install libopengl0 - - - 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 - - - name: Setup java (macOS) - if: runner.os == 'macOS' - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - ## - # CONFIGURE - ## - - - 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_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_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_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_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 }}") - { - Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe - echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV - echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV - echo "TrackFileAccess=false" >> $env:GITHUB_ENV - } - # Needed for ccache, but also speeds up compile - echo "UseMultiToolTask=true" >> $env:GITHUB_ENV - - - 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_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 - ## - - - name: Build - if: runner.os != 'Windows' - run: | - cmake --build ${{ env.BUILD_DIR }} - - - name: Build (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - cmake --build ${{ env.BUILD_DIR }} - - - name: Build (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} - - ## - # TEST - ## - - - name: Test - if: runner.os != 'Windows' - run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure - - - name: Test (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure - - - name: Test (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' - run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} - - ## - # 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: | - cmake --install ${{ env.BUILD_DIR }} - - cd ${{ env.INSTALL_DIR }} - chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" - - if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then - APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}' - else - APPLE_CODESIGN_ID='-' - fi - - sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" - mv "PrismLauncher.app" "Prism Launcher.app" - - - 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 - echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - 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: - - :memo: Sparkle Signature (ed25519): \`$signature\` - EOF - else - cat >> $GITHUB_STEP_SUMMARY << EOF - ### Artifact Information :information_source: - - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) - EOF - fi - - - name: Package (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - cmake --install ${{ env.BUILD_DIR }} - touch ${{ env.INSTALL_DIR }}/manifest.txt - for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Package (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} - - 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 - - - name: Fetch codesign certificate (Windows) - if: runner.os == 'Windows' - shell: bash # yes, we are not using MSYS2 or PowerShell here - run: | - echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx - - - name: Sign executable (Windows) - if: runner.os == 'Windows' - run: | - if (Get-Content ./codesign.pfx){ - cd ${{ env.INSTALL_DIR }} - # We ship the exact same executable for portable and non-portable editions, so signing just once is fine - SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } - - - name: Package (Windows MinGW-w64, portable) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - - - name: Package (Windows MSVC, portable) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - - Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - - 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" - - - name: Sign installer (Windows) - if: runner.os == 'Windows' - run: | - if (Get-Content ./codesign.pfx){ - SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } - - - name: Package AppImage (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 - shell: bash - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - - mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml - export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated - - export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" - - chmod +x linuxdeploy-*.AppImage - - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - - 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/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" - export LD_LIBRARY_PATH - - chmod +x AppImageUpdate-x86_64.AppImage - cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin - - export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" - - if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then - export SIGN=1 - export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }} - mkdir -p ~/.gnupg/ - echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key - gpg --import ~/.gnupg/private.key - else - echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY - fi - - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg - - mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" - - - name: Package (Linux, portable) - if: runner.os == 'Linux' - run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja - cmake --install ${{ env.BUILD_DIR }} - cmake --install ${{ env.BUILD_DIR }} --component portable - - mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib - mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib - - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - cd ${{ env.INSTALL_PORTABLE_DIR }} - tar -czf ../PrismLauncher-portable.tar.gz * - - ## - # UPLOAD BUILDS - ## - - - name: Upload binary tarball (macOS) - if: runner.os == 'macOS' - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher.zip - - - name: Upload binary zip (Windows) - if: runner.os == 'Windows' - 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@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@v4 - with: - name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher-Setup.exe - - - name: Upload binary tarball (Linux, portable, Qt 5) - if: runner.os == 'Linux' && matrix.qt_ver != 6 - 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, portable, Qt 6) - if: runner.os == 'Linux' && matrix.qt_ver != 5 - 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@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@v4 - with: - name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync - path: PrismLauncher-Linux-x86_64.AppImage.zsync - - - name: ccache stats (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - ccache -s - - flatpak: - runs-on: ubuntu-latest - container: - image: bilelmoussaoui/flatpak-github-actions:kde-6.7 - 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 - - nix: - name: Nix (${{ matrix.system }}) + name: Build (${{ matrix.artifact-name }}) strategy: fail-fast: false matrix: include: - os: ubuntu-22.04 - system: x86_64-linux + artifact-name: Linux + base-cmake-preset: linux - - os: macos-13 - system: x86_64-darwin + - os: windows-2022 + artifact-name: Windows-MinGW-w64 + base-cmake-preset: windows_mingw + msystem: CLANG64 + vcvars-arch: amd64_x86 + + - os: windows-11-arm + artifact-name: Windows-MinGW-arm64 + base-cmake-preset: windows_mingw + msystem: CLANGARM64 + vcvars-arch: arm64 + + - os: windows-2022 + artifact-name: Windows-MSVC + base-cmake-preset: windows_msvc + # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! + vcvars-arch: amd64 + + - os: windows-2022 + artifact-name: Windows-MSVC-arm64 + base-cmake-preset: windows_msvc_arm64_cross + vcvars-arch: amd64_arm64 + qt-architecture: win64_msvc2022_arm64_cross_compiled - os: macos-14 - system: aarch64-darwin + artifact-name: macOS + base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }} + macosx-deployment-target: 12.0 runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} + + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} + steps: - - name: Checkout repository + ## + # SETUP + ## + + - name: Checkout uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v30 - - # For PRs - - name: Setup Nix Magic Cache - uses: DeterminateSystems/magic-nix-cache-action@v8 - - # For in-tree builds - - name: Setup Cachix - uses: cachix/cachix-action@v15 with: - name: prismlauncher - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + submodules: true - - name: Run flake checks - run: | - nix flake check --print-build-logs --show-trace + - name: Setup dependencies + id: setup-dependencies + uses: ./.github/actions/setup-dependencies + with: + build-type: ${{ inputs.build-type || 'Debug' }} + msystem: ${{ matrix.msystem }} + vcvars-arch: ${{ matrix.vcvars-arch }} + qt-architecture: ${{ matrix.qt-architecture }} - - name: Build debug package - if: ${{ inputs.build_type == 'Debug' }} - run: | - nix build --print-build-logs .#prismlauncher-debug + ## + # BUILD + ## - - name: Build release package - if: ${{ inputs.build_type != 'Debug' }} + - name: Get CMake preset + id: cmake-preset + env: + BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }} + PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }} run: | - nix build --print-build-logs .#prismlauncher + echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT" + + - name: Run CMake workflow + env: + CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }} + run: | + cmake --workflow --preset "$CMAKE_PRESET" + + ## + # PACKAGE + ## + + - name: Get short version + id: short-version + shell: bash + run: | + echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Package (Linux) + if: ${{ runner.os == 'Linux' }} + uses: ./.github/actions/package/linux + with: + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + cmake-preset: ${{ steps.cmake-preset.outputs.preset }} + qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} + + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }} + + - name: Package (macOS) + if: ${{ runner.os == 'macOS' }} + uses: ./.github/actions/package/macos + with: + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + artifact-name: ${{ matrix.artifact-name }} + + 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 }} + sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }} + + - name: Package (Windows) + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/package/windows + with: + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + artifact-name: ${{ matrix.artifact-name }} + msystem: ${{ matrix.msystem }} + + windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }} + windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5255f865b..5a2ecbd6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,16 +1,60 @@ name: "CodeQL Code Scanning" -on: [ push, pull_request, workflow_dispatch ] +on: + push: + paths: + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/codeql" + - ".github/workflows/codeql.yml" + pull_request: + paths: + # File types + - "**.cpp" + - "**.h" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/codeql" + - ".github/workflows/codeql.yml" + workflow_dispatch: jobs: CodeQL: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 with: - submodules: 'true' + submodules: "true" - name: Initialize CodeQL uses: github/codeql-action/init@v3 @@ -19,17 +63,15 @@ jobs: queries: security-and-quality languages: cpp, java - - name: Install Dependencies - 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 libqt5networkauth5 libqt5networkauth5-dev + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies + with: + build-type: Debug - name: Configure and Build run: | - cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja - - cmake --build build + cmake --preset linux_debug + cmake --build --preset linux_debug - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 000000000..cab0edeb7 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,95 @@ +name: Flatpak + +on: + push: + # We don't do anything with these artifacts on releases. They go to Flathub + tags-ignore: + - "*" + paths: + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Build files + - "flatpak/" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/flatpak.yml" + pull_request: + paths: + # File types + - "**.cpp" + - "**.h" + + # Build files + - "flatpak/" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/flatpak.yml" + 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/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml new file mode 100644 index 000000000..d37c33761 --- /dev/null +++ b/.github/workflows/merge-blocking-pr.yml @@ -0,0 +1,62 @@ +name: Merged Blocking Pull Request Automation + +on: + pull_request_target: + types: + - closed + workflow_dispatch: + inputs: + pr_id: + description: Local Pull Request number to work on + required: true + type: number + +jobs: + update-blocked-status: + name: Update Blocked Status + runs-on: ubuntu-latest + + # a pr that was a `blocking:` label was merged. + # find the open pr's it was blocked by and trigger a refresh of their state + if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }} + + steps: + - name: Generate token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.PULL_REQUEST_APP_ID }} + private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} + + - name: Gather Dependent PRs + id: gather_deps + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} + run: | + blocked_prs=$( + gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ + | jq -c --argjson pr "$PR_NUMBER" ' + reduce ( .[] | select( + .body | + scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | + map(tonumber) | + any(.[]; . == $pr) + )) as $i ([]; . + [$i]) + ' + ) + { + echo "deps=$blocked_prs" + echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" + } >> "$GITHUB_OUTPUT" + + - name: Trigger Blocked PR Workflows for Dependants + if: fromJSON(steps.gather_deps.outputs.numdeps) > 0 + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + DEPS: ${{ steps.gather_deps.outputs.deps }} + run: | + while read -r pr ; do + gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr" + done < <(jq -c '.[].number' <<< "$DEPS") + diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..5a40ebb1f --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,143 @@ +name: Nix + +on: + push: + tags: + - "*" + paths: + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Build files + - "**.nix" + - "nix/" + - "flake.lock" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/nix.yml" + pull_request_target: + paths: + # File types + - "**.cpp" + - "**.h" + + # Build files + - "**.nix" + - "nix/" + - "flake.lock" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/nix.yml" + 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: Get merge commit + if: ${{ github.event_name == 'pull_request_target' }} + id: merge-commit + uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop + with: + pull-request-id: ${{ github.event.number }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }} + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v17 + with: + determinate: ${{ env.USE_DETERMINATE }} + + # For PRs + - name: Setup Nix Magic Cache + if: ${{ env.USE_DETERMINATE == 'true' }} + 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@v16 + 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 == 'true' }} + run: | + nix build \ + --no-link --print-build-logs --print-out-paths \ + .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY" + + - name: Build release package + if: ${{ env.DEBUG == 'false' }} + env: + TAG: ${{ github.ref_name }} + SYSTEM: ${{ matrix.system }} + run: | + nix build --no-link --print-out-paths .#prismlauncher \ + | tee -a "$GITHUB_STEP_SUMMARY" \ + | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM" diff --git a/.github/workflows/winget.yml b/.github/workflows/publish.yml similarity index 63% rename from .github/workflows/winget.yml rename to .github/workflows/publish.yml index eacf23099..8a7da812e 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/publish.yml @@ -1,13 +1,21 @@ -name: Publish to WinGet +name: Publish + on: release: - types: [released] + types: [ released ] + +permissions: + contents: read jobs: - publish: + winget: + name: Winget + runs-on: windows-latest + steps: - - uses: vedantmgoyal2009/winget-releaser@v2 + - name: Publish on Winget + uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/release.yml similarity index 76% rename from .github/workflows/trigger_release.yml rename to .github/workflows/release.yml index e800653e3..a93233dab 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/release.yml @@ -10,21 +10,8 @@ jobs: name: Build Release uses: ./.github/workflows/build.yml with: - build_type: Release - is_qt_cached: false - secrets: - SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} - WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} - WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - 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 }} - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} + build-type: Release + secrets: inherit create_release: needs: build_release @@ -47,10 +34,8 @@ 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-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync - mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -91,13 +76,15 @@ jobs: draft: true prerelease: false files: | - PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe + PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip + PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip + PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe @@ -105,5 +92,4 @@ jobs: PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-macOS-${{ env.VERSION }}.zip - PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip PrismLauncher-${{ env.VERSION }}.tar.gz 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 deleted file mode 100644 index 0b8386d69..000000000 --- a/.github/workflows/trigger_builds.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build Application - -on: - push: - branches-ignore: - - "renovate/**" - paths-ignore: - - "**.md" - - "**/LICENSE" - - "flake.lock" - - "packages/**" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" - pull_request: - paths-ignore: - - "**.md" - - "**/LICENSE" - - "flake.lock" - - "packages/**" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" - workflow_dispatch: - -jobs: - build_debug: - name: Build Debug - uses: ./.github/workflows/build.yml - with: - build_type: Debug - is_qt_cached: true - secrets: - SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} - WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} - WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - 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 }} - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 5e978f356..62852171b 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 + - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31 - uses: DeterminateSystems/update-flake-lock@v24 with: diff --git a/.gitignore b/.gitignore index b5523f685..00afabbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt +CMakeUserPresets.json /.project /.settings /.idea @@ -21,6 +22,7 @@ CMakeCache.txt /.vs cmake-build-*/ Debug +compile_commands.json # Build dirs build @@ -47,8 +49,12 @@ run/ # Nix/NixOS .direnv/ -.pre-commit-config.yaml +## Used when manually invoking stdenv phases +outputs/ +## Regular artifacts result +result-* +repl-result-* # Flatpak .flatpak-builder diff --git a/.gitmodules b/.gitmodules index 0f437d277..0c56d8768 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "libraries/tomlplusplus"] path = libraries/tomlplusplus url = https://github.com/marzer/tomlplusplus.git -[submodule "libraries/filesystem"] - path = libraries/filesystem - url = https://github.com/gulrak/filesystem [submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git @@ -22,3 +19,6 @@ [submodule "flatpak/shared-modules"] path = flatpak/shared-modules url = https://github.com/flathub/shared-modules.git +[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"] + path = libraries/qt-qrcodegenerator/QR-Code-generator + url = https://github.com/nayuki/QR-Code-generator diff --git a/CMakeLists.txt b/CMakeLists.txt index b677b0b7c..68d900c27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,13 +78,18 @@ 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() -# Fix build with Qt 5.13 -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") - -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000") # Fix aarch64 build for toml++ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") @@ -92,6 +97,12 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") +# Export compile commands for debug builds if we can (useful in LSPs like clangd) +# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html +if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() + option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser @@ -106,14 +117,14 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI 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") @@ -180,12 +191,13 @@ set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CAC set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 9) +set(Launcher_VERSION_MAJOR 10) set(Launcher_VERSION_MINOR 0) +set(Launcher_VERSION_PATCH 0) -set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") -set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") -set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") +set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") +set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") +set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") @@ -229,7 +241,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT 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) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader @@ -296,23 +308,11 @@ endif() # Find the required Qt parts 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 NetworkAuth) - - if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt5 1.3 QUIET) - endif() - if (NOT QuaZip-Qt5_FOUND) - set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) - set(FORCE_BUNDLED_QUAZIP 1) - endif() - - # Qt 6 sets these by default. Notably causes Windows APIs to use UNICODE strings. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth) + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL) + 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) @@ -326,29 +326,16 @@ else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() -if(Launcher_QT_VERSION_MAJOR EQUAL 5) - include(ECMQueryQt) - ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) - ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) - ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) -else() +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() -# NOTE: Qt 6 already sets this by default -if (Qt5_POSITION_INDEPENDENT_CODE) - SET(CMAKE_POSITION_INDEPENDENT_CODE ON) -endif() - if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find toml++ find_package(tomlplusplus 3.2.0 QUIET) - # Find ghc_filesystem - find_package(ghc_filesystem QUIET) - # Find cmark find_package(cmark QUIET) endif() @@ -366,7 +353,7 @@ set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) - set(Launcher_BUILD_UPDATER YES) + set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) @@ -397,8 +384,8 @@ if(UNIX AND APPLE) set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") - set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive") - set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") # directories to look for dependencies @@ -488,6 +475,7 @@ add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker +add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator if(FORCE_BUNDLED_ZLIB) message(STATUS "Using bundled zlib") @@ -543,12 +531,6 @@ else() endif() add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API -if (NOT ghc_filesystem_FOUND) - message(STATUS "Using bundled ghc_filesystem") - add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS -else() - message(STATUS "Using system ghc_filesystem") -endif() add_subdirectory(libraries/qdcss) # css parser ############################### Built Artifacts ############################### diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..f8e688b89 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "include": [ + "cmake/linuxPreset.json", + "cmake/macosPreset.json", + "cmake/windowsMinGWPreset.json", + "cmake/windowsMSVCPreset.json" + ] +} 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 111587060..1ebde116f 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## Prism Launcher Prism Launcher - Minecraft Launcher - Copyright (C) 2022-2024 Prism Launcher Contributors + Copyright (C) 2022-2025 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 @@ -108,7 +108,7 @@ Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. -## Qt 5/6 +## Qt 6 Copyright (C) 2022 The Qt Company Ltd and other contributors. Contact: https://www.qt.io/licensing @@ -362,28 +362,6 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## gulrak/filesystem - - Copyright (c) 2018, Steffen Schümann - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR 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. - ## Breeze icons Copyright (C) 2014 Uri Herrera and others @@ -425,3 +403,12 @@ You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . + +## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`) + + Copyright © 2024 Project Nayuki. (MIT License) + https://www.nayuki.io/page/qr-code-generator-library + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + - The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or 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. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index b48232b43..6bebcb80e 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -34,8 +34,8 @@ */ #include -#include "BuildConfig.h" #include +#include "BuildConfig.h" const Config BuildConfig; @@ -49,7 +49,7 @@ Config::Config() LAUNCHER_DOMAIN = "@Launcher_Domain@"; LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; LAUNCHER_GIT = "@Launcher_Git@"; - LAUNCHER_DESKTOPFILENAME = "@Launcher_DesktopFileName@"; + LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; USER_AGENT = "@Launcher_UserAgent@"; @@ -58,6 +58,7 @@ Config::Config() // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; + VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; @@ -74,14 +75,13 @@ Config::Config() MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; - if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) - { + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; - } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } - #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER +#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; @@ -89,27 +89,19 @@ Config::Config() GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" - if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") - || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") - || GIT_REFSPEC == QStringLiteral("") - || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) - { + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || + GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } - if (GIT_REFSPEC.startsWith("refs/heads/")) - { + if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; - VERSION_CHANNEL.remove("refs/heads/"); - } - else if (!GIT_COMMIT.isEmpty()) - { + VERSION_CHANNEL.remove("refs/heads/"); + } else if (!GIT_COMMIT.isEmpty()) { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); - } - else - { + } else { VERSION_CHANNEL = "unknown"; } @@ -136,7 +128,7 @@ Config::Config() QString Config::versionString() const { - return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR); + return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); } QString Config::printableVersionString() const @@ -144,8 +136,7 @@ QString Config::printableVersionString() const QString vstr = versionString(); // If the build is not a main release, append the channel - if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr) - { + if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { vstr += "-" + VERSION_CHANNEL; } return vstr; @@ -162,4 +153,3 @@ QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } - diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index ae705d098..b59adcb57 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -52,13 +52,15 @@ class Config { QString LAUNCHER_DOMAIN; QString LAUNCHER_CONFIGFILE; QString LAUNCHER_GIT; - QString LAUNCHER_DESKTOPFILENAME; + QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; /// The major version number. int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; + /// The patch version number. + int VERSION_PATCH; /** * The version channel diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 6d3845dfc..3a8c8fbfe 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -8,6 +8,8 @@ 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 diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json new file mode 100644 index 000000000..9cdf51649 --- /dev/null +++ b/cmake/commonPresets.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build", + "installDir": "install", + "cacheVariables": { + "Launcher_BUILD_PLATFORM": "custom" + } + }, + { + "name": "base_debug", + "hidden": true, + "inherits": [ + "base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "base_release", + "hidden": true, + "inherits": [ + "base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ENABLE_LTO": "ON" + } + }, + { + "name": "base_ci", + "hidden": true, + "inherits": [ + "base_release" + ], + "cacheVariables": { + "Launcher_BUILD_PLATFORM": "official", + "Launcher_FORCE_BUNDLED_LIBS": "ON" + } + } + ], + "testPresets": [ + { + "name": "base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "base_debug", + "hidden": true, + "inherits": [ + "base" + ], + "output": { + "debug": true + } + }, + { + "name": "base_release", + "hidden": true, + "inherits": [ + "base" + ] + } + ] +} diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json new file mode 100644 index 000000000..b8bfe4ff0 --- /dev/null +++ b/cmake/linuxPreset.json @@ -0,0 +1,180 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "generator": "Ninja", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Linux-Qt6", + "Launcher_ENABLE_JAVA_DOWNLOADER": "ON" + } + }, + { + "name": "linux_debug", + "inherits": [ + "base_debug", + "linux_base" + ], + "displayName": "Linux (Debug)" + }, + { + "name": "linux_release", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (Release)" + }, + { + "name": "linux_ci", + "inherits": [ + "base_ci", + "linux_base" + ], + "displayName": "Linux (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Linux-Qt6" + }, + "installDir": "/usr" + } + ], + "buildPresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux_debug", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (Debug)", + "configurePreset": "linux_debug" + }, + { + "name": "linux_release", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (Release)", + "configurePreset": "linux_release" + }, + { + "name": "linux_ci", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (CI)", + "configurePreset": "linux_ci" + } + ], + "testPresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux_debug", + "inherits": [ + "base_debug", + "linux_base" + ], + "displayName": "Linux (Debug)", + "configurePreset": "linux_debug" + }, + { + "name": "linux_release", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (Release)", + "configurePreset": "linux_release" + }, + { + "name": "linux_ci", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (CI)", + "configurePreset": "linux_ci" + } + ], + "workflowPresets": [ + { + "name": "linux_debug", + "displayName": "Linux (Debug)", + "steps": [ + { + "type": "configure", + "name": "linux_debug" + }, + { + "type": "build", + "name": "linux_debug" + }, + { + "type": "test", + "name": "linux_debug" + } + ] + }, + { + "name": "linux", + "displayName": "Linux (Release)", + "steps": [ + { + "type": "configure", + "name": "linux_release" + }, + { + "type": "build", + "name": "linux_release" + }, + { + "type": "test", + "name": "linux_release" + } + ] + }, + { + "name": "linux_ci", + "displayName": "Linux (CI)", + "steps": [ + { + "type": "configure", + "name": "linux_ci" + }, + { + "type": "build", + "name": "linux_ci" + }, + { + "type": "test", + "name": "linux_ci" + } + ] + } + ] +} diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json new file mode 100644 index 000000000..726949934 --- /dev/null +++ b/cmake/macosPreset.json @@ -0,0 +1,272 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "generator": "Ninja" + }, + { + "name": "macos_universal_base", + "hidden": true, + "inherits": [ + "macos_base" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "Launcher_BUILD_ARTIFACT": "macOS-Qt6" + } + }, + { + "name": "macos_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "macOS (Debug)" + }, + { + "name": "macos_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Release)" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "base_debug", + "macos_universal_base" + ], + "displayName": "macOS (Universal Binary, Debug)" + }, + { + "name": "macos_universal_release", + "inherits": [ + "base_release", + "macos_universal_base" + ], + "displayName": "macOS (Universal Binary, Release)" + }, + { + "name": "macos_ci", + "inherits": [ + "base_ci", + "macos_universal_base" + ], + "displayName": "macOS (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "macOS-Qt6" + } + } + ], + "buildPresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos_debug", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Debug)", + "configurePreset": "macos_debug" + }, + { + "name": "macos_release", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Release)", + "configurePreset": "macos_release" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Universal Binary, Debug)", + "configurePreset": "macos_universal_debug" + }, + { + "name": "macos_universal_release", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Universal Binary, Release)", + "configurePreset": "macos_universal_release" + }, + { + "name": "macos_ci", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (CI)", + "configurePreset": "macos_ci" + } + ], + "testPresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "MacOS (Debug)", + "configurePreset": "macos_debug" + }, + { + "name": "macos_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Release)", + "configurePreset": "macos_release" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "MacOS (Universal Binary, Debug)", + "configurePreset": "macos_universal_debug" + }, + { + "name": "macos_universal_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Universal Binary, Release)", + "configurePreset": "macos_universal_release" + }, + { + "name": "macos_ci", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (CI)", + "configurePreset": "macos_ci" + } + ], + "workflowPresets": [ + { + "name": "macos_debug", + "displayName": "macOS (Debug)", + "steps": [ + { + "type": "configure", + "name": "macos_debug" + }, + { + "type": "build", + "name": "macos_debug" + }, + { + "type": "test", + "name": "macos_debug" + } + ] + }, + { + "name": "macos", + "displayName": "macOS (Release)", + "steps": [ + { + "type": "configure", + "name": "macos_release" + }, + { + "type": "build", + "name": "macos_release" + }, + { + "type": "test", + "name": "macos_release" + } + ] + }, + { + "name": "macos_universal_debug", + "displayName": "macOS (Universal Binary, Debug)", + "steps": [ + { + "type": "configure", + "name": "macos_universal_debug" + }, + { + "type": "build", + "name": "macos_universal_debug" + }, + { + "type": "test", + "name": "macos_universal_debug" + } + ] + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary, Release)", + "steps": [ + { + "type": "configure", + "name": "macos_universal_release" + }, + { + "type": "build", + "name": "macos_universal_release" + }, + { + "type": "test", + "name": "macos_universal_release" + } + ] + }, + { + "name": "macos_ci", + "displayName": "macOS (CI)", + "steps": [ + { + "type": "configure", + "name": "macos_ci" + }, + { + "type": "build", + "name": "macos_ci" + }, + { + "type": "test", + "name": "macos_ci" + } + ] + } + ] +} diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json new file mode 100644 index 000000000..eb6a38b19 --- /dev/null +++ b/cmake/windowsMSVCPreset.json @@ -0,0 +1,311 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" + } + }, + { + "name": "windows_msvc_arm64_cross_base", + "hidden": true, + "inherits": [ + "windows_msvc_base" + ], + "architecture": "arm64", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "base_debug", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "generator": "Ninja" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)" + }, + { + "name": "windows_msvc_arm64_cross_debug", + "inherits": [ + "base_debug", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Debug)" + }, + { + "name": "windows_msvc_arm64_cross_release", + "inherits": [ + "base_release", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Release)" + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "base_ci", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" + } + }, + { + "name": "windows_msvc_arm64_cross_ci", + "inherits": [ + "base_ci", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" + } + } + ], + "buildPresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "configurePreset": "windows_msvc_debug", + "configuration": "Debug" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)", + "configurePreset": "windows_msvc_release", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_debug", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Debug)", + "configurePreset": "windows_msvc_arm64_cross_debug", + "configuration": "Debug", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_release", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Release)", + "configurePreset": "windows_msvc_arm64_cross_release", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "configurePreset": "windows_msvc_ci", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_ci", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, CI)", + "configurePreset": "windows_msvc_arm64_cross_ci", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + } + ], + "testPresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "base_debug", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "configurePreset": "windows_msvc_debug", + "configuration": "Debug" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)", + "configurePreset": "windows_msvc_release", + "configuration": "Release" + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "configurePreset": "windows_msvc_ci", + "configuration": "Release" + } + ], + "workflowPresets": [ + { + "name": "windows_msvc_debug", + "displayName": "Windows MSVC (Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_debug" + }, + { + "type": "build", + "name": "windows_msvc_debug" + }, + { + "type": "test", + "name": "windows_msvc_debug" + } + ] + }, + { + "name": "windows_msvc", + "displayName": "Windows MSVC (Release)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_release" + }, + { + "type": "build", + "name": "windows_msvc_release" + }, + { + "type": "test", + "name": "windows_msvc_release" + } + ] + }, + { + "name": "windows_msvc_arm64_cross_debug", + "displayName": "Windows MSVC (ARM64 cross, Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_debug" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_debug" + } + ] + }, + { + "name": "windows_msvc_arm64_cross", + "displayName": "Windows MSVC (ARM64 cross, Release)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_release" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_release" + } + ] + }, + { + "name": "windows_msvc_ci", + "displayName": "Windows MSVC (CI)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_ci" + }, + { + "type": "build", + "name": "windows_msvc_ci" + }, + { + "type": "test", + "name": "windows_msvc_ci" + } + ] + }, + { + "name": "windows_msvc_arm64_cross_ci", + "displayName": "Windows MSVC (ARM64 cross, CI)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_ci" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_ci" + } + ] + } + ] +} diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json new file mode 100644 index 000000000..984caadd6 --- /dev/null +++ b/cmake/windowsMinGWPreset.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "generator": "Ninja", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "base_debug", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "base_ci", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" + } + } + ], + "buildPresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)", + "configurePreset": "windows_mingw_debug" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)", + "configurePreset": "windows_mingw_release" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "configurePreset": "windows_mingw_ci" + } + ], + "testPresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "base_debug", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)", + "configurePreset": "windows_mingw_debug" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)", + "configurePreset": "windows_mingw_release" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "configurePreset": "windows_mingw_ci" + } + ], + "workflowPresets": [ + { + "name": "windows_mingw_debug", + "displayName": "Windows MinGW (Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_debug" + }, + { + "type": "build", + "name": "windows_mingw_debug" + }, + { + "type": "test", + "name": "windows_mingw_debug" + } + ] + }, + { + "name": "windows_mingw", + "displayName": "Windows MinGW (Release)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_release" + }, + { + "type": "build", + "name": "windows_mingw_release" + }, + { + "type": "test", + "name": "windows_mingw_release" + } + ] + }, + { + "name": "windows_mingw_ci", + "displayName": "Windows MinGW (CI)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_ci" + }, + { + "type": "build", + "name": "windows_mingw_ci" + }, + { + "type": "test", + "name": "windows_mingw_ci" + } + ] + } + ] +} diff --git a/default.nix b/default.nix index 6466507b7..5ecef5590 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1,4 @@ -(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 (fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; + sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; +}) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index a82e6f65f..5418557a3 100644 --- a/flake.lock +++ b/flake.lock @@ -1,29 +1,13 @@ { "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "libnbtplusplus": { "flake": false, "locked": { - "lastModified": 1699286814, - "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=", + "lastModified": 1744811532, + "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=", "owner": "PrismLauncher", "repo": "libnbtplusplus", - "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad", + "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78", "type": "github" }, "original": { @@ -32,28 +16,13 @@ "type": "github" } }, - "nix-filter": { - "locked": { - "lastModified": 1710156097, - "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", - "owner": "numtide", - "repo": "nix-filter", - "rev": "3342559a24e85fc164b295c3444e8a139924675b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "nix-filter", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1729256560, - "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", + "lastModified": 1745526057, + "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", + "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", "type": "github" }, "original": { @@ -63,12 +32,27 @@ "type": "github" } }, + "qt-qrcodegenerator": { + "flake": false, + "locked": { + "lastModified": 1737616857, + "narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=", + "owner": "nayuki", + "repo": "QR-Code-generator", + "rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138", + "type": "github" + }, + "original": { + "owner": "nayuki", + "repo": "QR-Code-generator", + "type": "github" + } + }, "root": { "inputs": { - "flake-compat": "flake-compat", "libnbtplusplus": "libnbtplusplus", - "nix-filter": "nix-filter", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "qt-qrcodegenerator": "qt-qrcodegenerator" } } }, diff --git a/flake.nix b/flake.nix index f4ca782ec..69abd78dd 100644 --- a/flake.nix +++ b/flake.nix @@ -16,25 +16,8 @@ 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"; + qt-qrcodegenerator = { + url = "github:nayuki/QR-Code-generator"; flake = false; }; }; @@ -44,9 +27,9 @@ self, nixpkgs, libnbtplusplus, - nix-filter, - ... + qt-qrcodegenerator, }: + let inherit (nixpkgs) lib; @@ -58,53 +41,151 @@ forAllSystems = lib.genAttrs systems; nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); in + { checks = forAllSystems ( system: + let - checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; }; + pkgs = nixpkgsFor.${system}; + llvm = pkgs.llvmPackages_19; in - lib.filterAttrs (_: lib.isDerivation) checks' + + { + formatting = + pkgs.runCommand "check-formatting" + { + nativeBuildInputs = with pkgs; [ + deadnix + llvm.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..." + find -type f -name '*.nix' -exec nixfmt --check {} + + + echo "Running statix" + statix check . + + touch $out + ''; + } ); devShells = forAllSystems ( system: + let pkgs = nixpkgsFor.${system}; + llvm = pkgs.llvmPackages_19; + + packages' = self.packages.${system}; + + welcomeMessage = '' + Welcome to the Prism Launcher repository! 🌈 + + We just set some things up for you. To get building, you can run: + + ``` + $ cd "$cmakeBuildDir" + $ ninjaBuildPhase + $ ninjaInstallPhase + ``` + + Feel free to ask any questions in our Discord server or Matrix space: + - https://prismlauncher.org/discord + - https://matrix.to/#/#prismlauncher:matrix.org + + And thanks for helping out :) + ''; + + # Re-use our package wrapper to wrap our development environment + qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: { + name = "qt-wrapper-env"; + + # Required to use script-based makeWrapper below + strictDeps = true; + + # We don't need/want the unwrapped Prism package + paths = [ ]; + + nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [ + # Ensure the wrapper is script based so it can be sourced + pkgs.makeWrapper + ]; + + # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10 + buildCommand = '' + makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out" + sed -i '/^exec/d' "$out" + ''; + }); in + { default = pkgs.mkShell { - inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ]; - buildInputs = with pkgs; [ + name = "prism-launcher"; + + inputsFrom = [ packages'.prismlauncher-unwrapped ]; + + packages = with pkgs; [ ccache - ninja + llvm.clang-tools ]; + + cmakeBuildType = "Debug"; + cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags; + dontFixCmake = true; + + shellHook = '' + echo "Sourcing ${qt-wrapper-env}" + source ${qt-wrapper-env} + + git submodule update --init --force + + if [ ! -f compile_commands.json ]; then + cmakeConfigurePhase + cd .. + ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json + fi + + echo ${lib.escapeShellArg welcomeMessage} + ''; }; } ); formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); - overlays.default = - final: prev: - let - version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; - in - { - prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { - inherit - libnbtplusplus - nix-filter - self - version - ; - }; - - prismlauncher = final.callPackage ./nix/wrapper.nix { }; + overlays.default = final: prev: { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit + libnbtplusplus + qt-qrcodegenerator + self + ; }; + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; + packages = forAllSystems ( system: + let pkgs = nixpkgsFor.${system}; @@ -117,6 +198,7 @@ default = prismPackages.prismlauncher; }; in + # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); @@ -124,16 +206,18 @@ # 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}; + packages' = self.packages.${system}; + legacyPackages' = self.legacyPackages.${system}; in + { - prismlauncher-debug = prismPackages.prismlauncher.override { - prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; + prismlauncher-debug = packages'.prismlauncher.override { + prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug; }; - prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { + prismlauncher-unwrapped-debug = packages'.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 09dd8d73b..136aef91a 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -1,6 +1,6 @@ id: org.prismlauncher.PrismLauncher runtime: org.kde.Platform -runtime-version: 6.7 +runtime-version: '6.8' sdk: org.kde.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.openjdk17 @@ -19,6 +19,12 @@ finish-args: - --filesystem=xdg-download:ro # FTBApp import - --filesystem=~/.ftba:ro + # 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) @@ -27,11 +33,16 @@ 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 build-options: env: @@ -47,18 +58,14 @@ modules: config-opts: - -DCMAKE_BUILD_TYPE=RelWithDebInfo - -DBUILD_SHARED_LIBS:BOOL=ON - - -DGLFW_USE_WAYLAND:BOOL=ON + - -DGLFW_BUILD_WAYLAND:BOOL=ON - -DGLFW_BUILD_DOCS:BOOL=OFF sources: - type: git url: https://github.com/glfw/glfw.git - commit: 3fa2360720eeba1964df3c0ecf4b5df8648a8e52 + 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 @@ -68,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 @@ -91,8 +98,8 @@ modules: sources: - type: archive dest-filename: gamemode.tar.gz - url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.1 - sha256: 969cf85b5ca3944f3e315cd73a0ee9bea4f9c968cd7d485e9f4745bc1e679c4e + url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2 + sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60 x-checker-data: type: json url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest diff --git a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch b/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch deleted file mode 100644 index 9130e856c..000000000 --- a/flatpak/patches/0003-Don-t-crash-on-calls-to-focus-or-icon.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/src/wl_window.c b/src/wl_window.c -index 52d3b9eb..4ac4eb5d 100644 ---- a/src/wl_window.c -+++ b/src/wl_window.c -@@ -2117,8 +2117,7 @@ void _glfwSetWindowTitleWayland(_GLFWwindow* window, const char* title) - void _glfwSetWindowIconWayland(_GLFWwindow* window, - int count, const GLFWimage* images) - { -- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, -- "Wayland: The platform does not support setting the window icon"); -+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the window icon\n"); - } - - void _glfwGetWindowPosWayland(_GLFWwindow* window, int* xpos, int* ypos) -@@ -2361,8 +2360,7 @@ void _glfwRequestWindowAttentionWayland(_GLFWwindow* window) - - void _glfwFocusWindowWayland(_GLFWwindow* window) - { -- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, -- "Wayland: The platform does not support setting the input focus"); -+ fprintf(stderr, "!!! Ignoring Error: Wayland: The platform does not support setting the input focus\n"); - } - - void _glfwSetWindowMonitorWayland(_GLFWwindow* window, diff --git a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch b/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch deleted file mode 100644 index b031d739f..000000000 --- a/flatpak/patches/0005-Add-warning-about-being-an-unofficial-patch.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/src/init.c b/src/init.c -index 06dbb3f2..a7c6da86 100644 ---- a/src/init.c -+++ b/src/init.c -@@ -449,6 +449,12 @@ GLFWAPI int glfwInit(void) - _glfw.initialized = GLFW_TRUE; - - glfwDefaultWindowHints(); -+ -+ fprintf(stderr, "!!! Patched GLFW from https://github.com/Admicos/minecraft-wayland\n" -+ "!!! If any issues with the window, or some issues with rendering, occur, " -+ "first try with the built-in GLFW, and if that solves the issue, report there first.\n" -+ "!!! Use outside Minecraft is untested, and things might break.\n"); -+ - return GLFW_TRUE; - } - diff --git a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch b/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch deleted file mode 100644 index 4eeb81309..000000000 --- a/flatpak/patches/0007-Platform-Prefer-Wayland-over-X11.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/src/platform.c b/src/platform.c -index c5966ae7..3e7442f9 100644 ---- a/src/platform.c -+++ b/src/platform.c -@@ -49,12 +49,12 @@ static const struct - #if defined(_GLFW_COCOA) - { GLFW_PLATFORM_COCOA, _glfwConnectCocoa }, - #endif --#if defined(_GLFW_X11) -- { GLFW_PLATFORM_X11, _glfwConnectX11 }, --#endif - #if defined(_GLFW_WAYLAND) - { GLFW_PLATFORM_WAYLAND, _glfwConnectWayland }, - #endif -+#if defined(_GLFW_X11) -+ { GLFW_PLATFORM_X11, _glfwConnectX11 }, -+#endif - }; - - GLFWbool _glfwSelectPlatform(int desiredID, _GLFWplatform* platform) diff --git a/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch new file mode 100644 index 000000000..70cec9981 --- /dev/null +++ b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch @@ -0,0 +1,59 @@ +From 9997ae55a47de469ea26f8437c30b51483abda5f Mon Sep 17 00:00:00 2001 +From: Dan Klishch +Date: Sat, 30 Sep 2023 23:38:05 -0400 +Subject: Defer setting cursor position until the cursor is locked + +--- + src/wl_platform.h | 3 +++ + src/wl_window.c | 14 ++++++++++++-- + 2 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/src/wl_platform.h b/src/wl_platform.h +index ca34f66e..cd1f227f 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -403,6 +403,9 @@ typedef struct _GLFWwindowWayland + int scaleSize; + int compositorPreferredScale; + ++ double askedCursorPosX, askedCursorPosY; ++ GLFWbool didAskForSetCursorPos; ++ + struct zwp_relative_pointer_v1* relativePointer; + struct zwp_locked_pointer_v1* lockedPointer; + struct zwp_confined_pointer_v1* confinedPointer; +diff --git a/src/wl_window.c b/src/wl_window.c +index 1de26558..0df16747 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2586,8 +2586,9 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos) + + void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the cursor position"); ++ window->wl.didAskForSetCursorPos = true; ++ window->wl.askedCursorPosX = x; ++ window->wl.askedCursorPosY = y; + } + + void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode) +@@ -2819,6 +2820,15 @@ static const struct zwp_relative_pointer_v1_listener relativePointerListener = + static void lockedPointerHandleLocked(void* userData, + struct zwp_locked_pointer_v1* lockedPointer) + { ++ _GLFWwindow* window = userData; ++ ++ if (window->wl.didAskForSetCursorPos) ++ { ++ window->wl.didAskForSetCursorPos = false; ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(window->wl.askedCursorPosX), ++ wl_fixed_from_double(window->wl.askedCursorPosY)); ++ } + } + + static void lockedPointerHandleUnlocked(void* userData, +-- +2.42.0 + diff --git a/flatpak/patches/weird_libdecor.patch b/flatpak/patches/weird_libdecor.patch deleted file mode 100644 index 3a400b820..000000000 --- a/flatpak/patches/weird_libdecor.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/src/libdecor.c b/src/libdecor.c -index a9c1106..1aa38b3 100644 ---- a/src/libdecor.c -+++ b/src/libdecor.c -@@ -1391,22 +1391,32 @@ calculate_priority(const struct libdecor_plugin_description *plugin_description) - static bool - check_symbol_conflicts(const struct libdecor_plugin_description *plugin_description) - { -+ bool ret = true; - char * const *symbol; -+ void* main_prog = dlopen(NULL, RTLD_LAZY); -+ if (!main_prog) { -+ fprintf(stderr, "Plugin \"%s\" couldn't check conflicting symbols: \"%s\".\n", -+ plugin_description->description, dlerror()); -+ return false; -+ } -+ - - symbol = plugin_description->conflicting_symbols; - while (*symbol) { - dlerror(); -- dlsym (RTLD_DEFAULT, *symbol); -+ dlsym (main_prog, *symbol); - if (!dlerror()) { - fprintf(stderr, "Plugin \"%s\" uses conflicting symbol \"%s\".\n", - plugin_description->description, *symbol); -- return false; -+ ret = false; -+ break; - } - - symbol++; - } - -- return true; -+ dlclose(main_prog); -+ return ret; - } - - static struct plugin_loader * diff --git a/flatpak/shared-modules b/flatpak/shared-modules index f2b0c16a2..73f08ed2c 160000 --- a/flatpak/shared-modules +++ b/flatpak/shared-modules @@ -1 +1 @@ -Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32 +Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ea749ca4c..0daab026c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -48,6 +48,7 @@ #include "net/PasteUpload.h" #include "pathmatcher/MultiMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h" +#include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" @@ -58,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" @@ -97,6 +96,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +128,7 @@ #include #include +#include #include "SysInfo.h" #ifdef Q_OS_LINUX @@ -154,9 +155,15 @@ #endif #if defined Q_OS_WIN32 -#include -#include "WindowsConsole.h" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN #endif +#include +#include +#include "console/WindowsConsole.h" +#endif + +#include "console/Console.h" #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) @@ -165,6 +172,63 @@ static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; +static bool isANSIColorConsole; + +static QString defaultLogFormat = QStringLiteral( + "%{time process}" + " " + "%{if-debug}Debug:%{endif}" + "%{if-info}Info:%{endif}" + "%{if-warning}Warning:%{endif}" + "%{if-critical}Critical:%{endif}" + "%{if-fatal}Fatal:%{endif}" + " " + "%{if-category}[%{category}] %{endif}" + "%{message}" + " " + "(%{function}:%{line})"); + +#define ansi_reset "\x1b[0m" +#define ansi_bold "\x1b[1m" +#define ansi_reset_bold "\x1b[22m" +#define ansi_faint "\x1b[2m" +#define ansi_italic "\x1b[3m" +#define ansi_red_fg "\x1b[31m" +#define ansi_green_fg "\x1b[32m" +#define ansi_yellow_fg "\x1b[33m" +#define ansi_blue_fg "\x1b[34m" +#define ansi_purple_fg "\x1b[35m" +#define ansi_inverse "\x1b[7m" + +// clang-format off +static QString ansiLogFormat = QStringLiteral( + ansi_faint "%{time process}" ansi_reset + " " + "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" + "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" + "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" + "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" + "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" + " " + "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" + "%{message}" + " " + ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset +); +// clang-format on + +#undef ansi_inverse +#undef ansi_purple_fg +#undef ansi_blue_fg +#undef ansi_yellow_fg +#undef ansi_green_fg +#undef ansi_red_fg +#undef ansi_italic +#undef ansi_faint +#undef ansi_bold +#undef ansi_reset_bold +#undef ansi_reset + namespace { /** This is used so that we can output to the log file in addition to the CLI. */ @@ -173,11 +237,24 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + if (isANSIColorConsole) { + // ensure default is set for log file + qSetMessagePattern(defaultLogFormat); + } + QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); + + if (isANSIColorConsole) { + // format ansi for console; + qSetMessagePattern(ansiLogFormat); + out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + } + QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } @@ -218,15 +295,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // attach the parent console if stdout not already captured if (AttachWindowsConsole()) { consoleAttached = true; + if (auto err = EnableAnsiSupport(); !err) { + isANSIColorConsole = true; + } else { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +#else + if (console::isConsole()) { + isANSIColorConsole = true; } #endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); - setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); - startTime = QDateTime::currentDateTime(); + setDesktopFileName(BuildConfig.LAUNCHER_APPID); + m_startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); @@ -242,6 +329,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "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" } }); @@ -257,6 +345,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) 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"); @@ -271,8 +363,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // error if --launch is missing with --server or --profile - if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { - std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; + 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; } @@ -369,19 +462,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { + bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; - m_peerInstance->sendMessage(activate.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); - m_peerInstance->sendMessage(import.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { @@ -397,10 +491,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } - m_peerInstance->sendMessage(launch.serialize(), timeout); + if (m_offline) { + launch.args["offline_enabled"] = "true"; + launch.args["offline_name"] = m_offlineName; + } + sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); + } + if (sentMessage) { + m_status = Application::Succeeded; + return; + } else { + std::cerr << "Unable to redirect command to already running instance\n"; + // C function not Qt function - event loop not started yet + ::exit(1); } - m_status = Application::Succeeded; - return; } } @@ -431,27 +535,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) return; } qInstallMessageHandler(appDebugOutput); - - qSetMessagePattern( - "%{time process}" - " " - "%{if-debug}D%{endif}" - "%{if-info}I%{endif}" - "%{if-warning}W%{endif}" - "%{if-critical}C%{endif}" - "%{if-fatal}F%{endif}" - " " - "|" - " " - "%{if-category}[%{category}]: %{endif}" - "%{message}"); + qSetMessagePattern(defaultLogFormat); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() @@ -459,7 +550,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { - qDebug() << "Found" << logRulesPath << "..."; + qInfo() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } @@ -470,28 +561,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules - qDebug() << "Loading logging rules from:" << logRulesPath; + qInfo() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; - qDebug() << "Setting log rules:"; + qInfo() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); - qDebug() << " " << rule; + qInfo() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } - qDebug() << "<> Log initialized."; + qInfo() << "<> Log initialized."; } { @@ -508,33 +599,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - 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; - qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; - qDebug() << "Compiled for : " << BuildConfig.systemID(); - qDebug() << "Compiled by : " << BuildConfig.compilerID(); - qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; - qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); + qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qInfo() << "Version : " << BuildConfig.printableVersionString(); + qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM; + qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT; + qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qInfo() << "Compiled for : " << BuildConfig.systemID(); + qInfo() << "Compiled by : " << BuildConfig.compilerID(); + qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { - qDebug() << "Work dir before adjustment : " << origcwdPath; - qDebug() << "Work dir after adjustment : " << QDir::currentPath(); - qDebug() << "Adjusted by : " << adjustedBy; + qInfo() << "Work dir before adjustment : " << origcwdPath; + qInfo() << "Work dir after adjustment : " << QDir::currentPath(); + qInfo() << "Adjusted by : " << adjustedBy; } else { - qDebug() << "Work dir : " << QDir::currentPath(); + qInfo() << "Work dir : " << QDir::currentPath(); } - qDebug() << "Binary path : " << binPath; - qDebug() << "Application root path : " << m_rootPath; + qInfo() << "Binary path : " << binPath; + qInfo() << "Application root path : " << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { - qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; + qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { - qDebug() << "Address of server to join :" << m_serverToJoin; + qInfo() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { - qDebug() << "Name of the world to join :" << m_worldToJoin; + qInfo() << "Name of the world to join :" << m_worldToJoin; } - qDebug() << "<> Paths set."; + qInfo() << "<> Paths set."; } if (m_liveCheck) { @@ -605,6 +696,7 @@ 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"); @@ -698,7 +790,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ToolbarsLocked", false); + // Instance m_settings->registerSetting("InstSortMode", "Name"); + m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry @@ -790,8 +884,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(); @@ -800,7 +892,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) PixmapCache::setInstance(new PixmapCache(this)); - qDebug() << "<> Settings loaded."; + qInfo() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY @@ -816,7 +908,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); - qDebug() << "<> Network done."; + qInfo() << "<> Network done."; } // load translations @@ -824,8 +916,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_translations.reset(new TranslationsModel("translations")); auto bcp47Name = m_settings->get("Language").toString(); m_translations->selectLanguage(bcp47Name); - qDebug() << "Your language is" << bcp47Name; - qDebug() << "<> Translations loaded."; + qInfo() << "Your language is" << bcp47Name; + qInfo() << "<> Translations loaded."; } // Instance icons @@ -835,8 +927,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 initialized."; + [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); + qInfo() << "<> Instance icons initialized."; } // Themes @@ -848,25 +940,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // instance path: check for problems with '!' in instance path and warn the user in the log // and remember that we have to show him a dialog when the gui starts (if it does so) QString instDir = InstDirSetting->get().toString(); - qDebug() << "Instance path : " << instDir; + qInfo() << "Instance path : " << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } m_instances.reset(new InstanceList(m_settings, instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); - qDebug() << "Loading Instances..."; + qInfo() << "Loading Instances..."; m_instances->loadList(); - qDebug() << "<> Instances loaded."; + qInfo() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); - qDebug() << "Loading accounts..."; + qInfo() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); - qDebug() << "<> Accounts loaded."; + qInfo() << "<> Accounts loaded."; } // init the http meta cache @@ -887,7 +979,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); - qDebug() << "<> Cache initialized."; + qInfo() << "<> Cache initialized."; } // now we have network, download translation updates @@ -1070,11 +1162,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { - bool javaRequired = [&]() { - if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) { + bool javaRequired = [this]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { return false; } - bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); + bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } @@ -1088,8 +1180,8 @@ bool Application::createSetupWizard() QString actualPath = FS::ResolveExecutable(currentJavaPath); return actualPath.isNull(); }(); - bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() && - !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool(); + bool 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()); @@ -1101,8 +1193,16 @@ bool Application::createSetupWizard() // set default theme after going into theme wizard if (!validIcons) settings()->set("IconTheme", QString("pe_colored")); - if (!validWidgets) - settings()->set("ApplicationTheme", QString("system")); + if (!validWidgets) { +#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + const QString style = + QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QStringLiteral("dark") : QStringLiteral("bright"); +#else + const QString style = QStringLiteral("system"); +#endif + + settings()->set("ApplicationTheme", style); + } m_themeManager->applyCurrentlySelectedTheme(true); @@ -1169,6 +1269,9 @@ bool Application::event(QEvent* event) #endif if (event->type() == QEvent::FileOpen) { + if (!m_mainWindow) { + showMainWindow(false); + } auto ev = static_cast(event); m_mainWindow->processURLs({ ev->url() }); } @@ -1209,7 +1312,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, targetToJoin, accountToUse); + launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); return; } } @@ -1302,12 +1405,17 @@ void Application::messageReceived(const QByteArray& message) qWarning() << "Received" << command << "message without a zip path/URL."; return; } + if (!m_mainWindow) { + showMainWindow(false); + } m_mainWindow->processURLs({ normalizeImportUrl(url) }); } 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()) { @@ -1337,7 +1445,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; } @@ -1375,11 +1483,17 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse) +bool Application::launch(InstancePtr instance, + bool online, + bool demo, + 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) { @@ -1395,6 +1509,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); + controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { @@ -1404,7 +1519,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); addRunningInstance(); - controller->start(); + QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); @@ -1422,9 +1537,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(); } @@ -1478,12 +1595,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(); @@ -1503,6 +1622,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 @@ -1560,6 +1680,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa if (!instance) return nullptr; auto id = instance->id(); + QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; @@ -1597,6 +1718,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) { @@ -1844,7 +1966,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(); @@ -1910,4 +2032,4 @@ bool Application::checkQSavePath(QString path) } } return false; -} \ No newline at end of file +} diff --git a/launcher/Application.h b/launcher/Application.h index 692625f4a..12f41509c 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -112,7 +112,7 @@ class Application : public QApplication { std::shared_ptr settings() const { return m_settings; } - qint64 timeSinceStart() const { return startTime.msecsTo(QDateTime::currentDateTime()); } + qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } QIcon getThemedIcon(const QString& name); @@ -211,7 +211,8 @@ class Application : public QApplication { bool online = true, bool demo = false, MinecraftTarget::Ptr targetToJoin = nullptr, - MinecraftAccountPtr accountToUse = nullptr); + MinecraftAccountPtr accountToUse = nullptr, + const QString& offlineName = QString()); bool kill(InstancePtr instance); void closeCurrentWindow(); @@ -236,7 +237,7 @@ class Application : public QApplication { bool shouldExitNow() const; private: - QDateTime startTime; + QDateTime m_startTime; shared_qobject_ptr m_network; @@ -279,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; @@ -300,6 +302,8 @@ class Application : public QApplication { 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; diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 69cf95e3c..70e0f9dc1 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -42,8 +42,8 @@ #include #include #include -#include +#include "Application.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -174,6 +174,12 @@ void BaseInstance::copyManagedPack(BaseInstance& other) m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); + + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && + m_settings->get("OverrideJavaLocation").toBool()) { + m_settings->set("OverrideJavaLocation", false); + m_settings->set("JavaPath", ""); + } } int BaseInstance::getConsoleMaxLines() const @@ -386,6 +392,12 @@ void BaseInstance::setName(QString val) emit propertiesChanged(this); } +bool BaseInstance::syncInstanceDirName(const QString& newRoot) const +{ + auto oldRoot = instanceRoot(); + return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); +} + QString BaseInstance::name() const { return m_settings->get("name").toString(); @@ -411,3 +423,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 2be28d1ec..1acf1afe0 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -126,6 +126,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this -DataMigrationTask::DataMigrationTask(QObject* parent, - const QString& sourcePath, - const QString& targetPath, - const IPathMatcher::Ptr pathMatcher) - : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) + : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { - m_copy.matcher(m_pathMatcher.get()).whitelist(true); + m_copy.matcher(m_pathMatcher).whitelist(true); } void DataMigrationTask::executeTask() @@ -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); @@ -40,11 +37,7 @@ void DataMigrationTask::dryRunFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Failed to scan source path.")); return; } @@ -60,7 +53,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); @@ -78,11 +71,7 @@ void DataMigrationTask::copyFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Some paths could not be copied!")); return; } 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/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index df06c3c75..cebe82eda 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -40,12 +40,11 @@ #include #include #include -#include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" -FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {} +FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {} // NOTE: Sadly, we have to do sorting ourselves. bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const { @@ -104,10 +103,10 @@ QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const if (index.column() == 0 && role == Qt::CheckStateRole) { QFileSystemModel* fsm = qobject_cast(sourceModel()); auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto cover = blocked.cover(blockedPath); + auto cover = m_blocked.cover(blockedPath); if (!cover.isNull()) { return QVariant(Qt::Unchecked); - } else if (blocked.exists(blockedPath)) { + } else if (m_blocked.exists(blockedPath)) { return QVariant(Qt::PartiallyChecked); } else { return QVariant(Qt::Checked); @@ -130,7 +129,7 @@ bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, i QString FileIgnoreProxy::relPath(const QString& path) const { - return QDir(root).relativeFilePath(path); + return QDir(m_root).relativeFilePath(path); } bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) @@ -146,18 +145,18 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) bool changed = false; if (state == Qt::Unchecked) { // blocking a path - auto& node = blocked.insert(blockedPath); + auto& node = m_blocked.insert(blockedPath); // get rid of all blocked nodes below node.clear(); changed = true; } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { - if (!blocked.remove(blockedPath)) { - auto cover = blocked.cover(blockedPath); + if (!m_blocked.remove(blockedPath)) { + auto cover = m_blocked.cover(blockedPath); qDebug() << "Blocked by cover" << cover; // uncover - blocked.remove(cover); + m_blocked.remove(cover); // block all contents, except for any cover - QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover)); + QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover)); QModelIndex doing = rootIndex; int row = 0; QStack todo; @@ -179,7 +178,7 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) todo.push(node); } else { // or just block this one. - blocked.insert(relpath); + m_blocked.insert(relpath); } row++; } @@ -229,7 +228,7 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) return false; } auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto found = blocked.find(blockedPath); + auto found = m_blocked.find(blockedPath); if (found) { return !found->leaf(); } @@ -239,8 +238,8 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) void FileIgnoreProxy::setBlockedPaths(QStringList paths) { beginResetModel(); - blocked.clear(); - blocked.insert(paths); + m_blocked.clear(); + m_blocked.insert(paths); endResetModel(); } @@ -270,7 +269,28 @@ bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); } -bool FileIgnoreProxy::filterFile(const QString& fileName) const +bool FileIgnoreProxy::filterFile(const QFileInfo& file) const { - return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName)); + return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); +} + +void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) +{ + QFile ignoreFile(fileName); + if (!ignoreFile.open(QIODevice::ReadOnly)) { + return; + } + auto ignoreData = ignoreFile.readAll(); + auto string = QString::fromUtf8(ignoreData); + setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); +} + +void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) +{ + auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8(); + try { + FS::write(fileName, ignoreData); + } catch (const Exception& e) { + qWarning() << e.cause(); + } } diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index e01a2651e..5184fc354 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -61,15 +61,19 @@ class FileIgnoreProxy : public QSortFilterProxyModel { void setBlockedPaths(QStringList paths); - inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } - inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } + inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; } + inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; } // list of file names that need to be removed completely from model inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } - bool filterFile(const QString& fileName) const; + bool filterFile(const QFileInfo& fileName) const; + + void loadBlockedPathsFromFile(const QString& fileName); + + void saveBlockedPathsToFile(const QString& fileName); protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; @@ -78,8 +82,8 @@ class FileIgnoreProxy : public QSortFilterProxyModel { bool ignoreFile(QFileInfo file) const; private: - const QString root; - SeparatorPrefixTree<'/'> blocked; + const QString m_root; + SeparatorPrefixTree<'/'> m_blocked; QStringList m_ignoreFiles; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index a02a0d642..a8e089f2d 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -77,24 +77,8 @@ #include #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif // clone #if defined(Q_OS_LINUX) @@ -341,7 +325,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; @@ -428,7 +412,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; @@ -523,7 +507,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; @@ -605,7 +589,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(); @@ -695,9 +679,6 @@ bool deletePath(QString path) bool trash(QString path, QString* pathInTrash) { -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - return false; -#else // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; @@ -706,7 +687,6 @@ bool trash(QString path, QString* pathInTrash) return false; #endif return QFile::moveToTrash(path, pathInTrash); -#endif } QString PathCombine(const QString& path1, const QString& path2) @@ -740,11 +720,7 @@ int pathDepth(const QString& path) QFileInfo info(path); -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); -#endif int numParts = parts.length(); numParts -= parts.count("."); @@ -764,11 +740,7 @@ QString pathTruncate(const QString& path, int depth) return pathTruncate(trunc, depth); } -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); -#endif if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); @@ -946,7 +918,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; - QFile info = content.path() + "/Info.plist"; + QFile info(content.path() + "/Info.plist"); if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; @@ -1291,7 +1263,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; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 4aa5596ae..b1108eded 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -115,7 +115,7 @@ class copy : public QObject { m_followSymlinks = follow; return *this; } - copy& matcher(const IPathMatcher* filter) + copy& matcher(IPathMatcher::Ptr filter) { m_matcher = filter; return *this; @@ -147,7 +147,7 @@ class copy : public QObject { private: bool m_followSymlinks = true; - const IPathMatcher* m_matcher = nullptr; + IPathMatcher::Ptr m_matcher = nullptr; bool m_whitelist = false; bool m_overwrite = false; QDir m_src; @@ -209,7 +209,7 @@ class create_link : public QObject { m_useHardLinks = useHard; return *this; } - create_link& matcher(const IPathMatcher* filter) + create_link& matcher(IPathMatcher::Ptr filter) { m_matcher = filter; return *this; @@ -260,7 +260,7 @@ class create_link : public QObject { private: bool m_useHardLinks = false; - const IPathMatcher* m_matcher = nullptr; + IPathMatcher::Ptr m_matcher = nullptr; bool m_whitelist = false; bool m_recursive = true; @@ -491,7 +491,7 @@ class clone : public QObject { m_src.setPath(src); m_dst.setPath(dst); } - clone& matcher(const IPathMatcher* filter) + clone& matcher(IPathMatcher::Ptr filter) { m_matcher = filter; return *this; @@ -517,7 +517,7 @@ class clone : public QObject { bool operator()(const QString& offset, bool dryRun = false); private: - const IPathMatcher* m_matcher = nullptr; + IPathMatcher::Ptr m_matcher = nullptr; bool m_whitelist = false; QDir m_src; QDir m_dst; diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 1c2539e08..29c71c012 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -36,6 +36,8 @@ #include "GZip.h" #include #include +#include +#include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { @@ -136,3 +138,81 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) } return true; } + +int inf(QFile* source, std::function handleBlock) +{ + constexpr auto CHUNK = 16384; + int ret; + unsigned have; + z_stream strm; + memset(&strm, 0, sizeof(strm)); + char in[CHUNK]; + unsigned char out[CHUNK]; + + ret = inflateInit2(&strm, (16 + MAX_WBITS)); + if (ret != Z_OK) + return ret; + + /* decompress until deflate stream ends or end of file */ + do { + strm.avail_in = source->read(in, CHUNK); + if (source->error()) { + (void)inflateEnd(&strm); + return Z_ERRNO; + } + if (strm.avail_in == 0) + break; + strm.next_in = reinterpret_cast(in); + + /* run inflate() on input until output buffer not full */ + do { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate(&strm, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); /* state not clobbered */ + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; /* and fall through */ + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void)inflateEnd(&strm); + return ret; + } + have = CHUNK - strm.avail_out; + if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { + (void)inflateEnd(&strm); + return Z_OK; + } + + } while (strm.avail_out == 0); + + /* done when inflate() says it's done */ + } while (ret != Z_STREAM_END); + + /* clean up and return */ + (void)inflateEnd(&strm); + return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; +} + +QString zerr(int ret) +{ + switch (ret) { + case Z_ERRNO: + return QObject::tr("error handling file"); + case Z_STREAM_ERROR: + return QObject::tr("invalid compression level"); + case Z_DATA_ERROR: + return QObject::tr("invalid or incomplete deflate data"); + case Z_MEM_ERROR: + return QObject::tr("out of memory"); + case Z_VERSION_ERROR: + return QObject::tr("zlib version mismatch!"); + } + return {}; +} + +QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) +{ + auto ret = inf(source, handleBlock); + return zerr(ret); +} \ No newline at end of file diff --git a/launcher/GZip.h b/launcher/GZip.h index 0bdb70407..b736ca93f 100644 --- a/launcher/GZip.h +++ b/launcher/GZip.h @@ -1,8 +1,11 @@ #pragma once #include +#include -class GZip { - public: - static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); - static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); -}; +namespace GZip { + +bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); +bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); +QString readGzFileByBlocks(QFile* source, std::function handleBlock); + +} // namespace GZip diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 0220a4144..fb5963532 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -43,7 +43,7 @@ void InstanceCopyTask::executeTask() m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); - folderClone.matcher(m_matcher.get()); + folderClone.matcher(m_matcher); folderClone(true); setProgress(0, folderClone.totalCloned()); @@ -72,7 +72,7 @@ void InstanceCopyTask::executeTask() } 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.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); folderLink(true); setProgress(0, m_progressTotal + folderLink.totalToLink()); @@ -91,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!"; } @@ -127,7 +127,7 @@ void InstanceCopyTask::executeTask() return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher.get()); + folderCopy.followSymlinks(false).matcher(m_matcher); folderCopy(true); setProgress(0, folderCopy.totalCopied()); diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 0f7f1020d..3aba13e5c 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -28,7 +28,7 @@ class InstanceCopyTask : public InstanceTask { InstancePtr m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; - std::unique_ptr m_matcher; + IPathMatcher::Ptr m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 3e7b3142f..bd3514798 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -61,6 +61,6 @@ void InstanceCreationTask::executeTask() return; } } - - emitSucceeded(); + if (!m_abort) + emitSucceeded(); } diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp new file mode 100644 index 000000000..8be0dccac --- /dev/null +++ b/launcher/InstanceDirUpdate.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * 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 "InstanceDirUpdate.h" + +#include + +#include "Application.h" +#include "FileSystem.h" + +#include "InstanceList.h" +#include "ui/dialogs/CustomMessageBox.h" + +QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent) +{ + if (oldName == newName) + return QString(); + + QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); + if (renamingMode == "MetadataOnly") + return QString(); + + auto oldRoot = instance->instanceRoot(); + auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); + auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); + if (oldRoot == newRoot) + return QString(); + if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) + return QString(); + + // Check for conflict + if (QDir(newRoot).exists()) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot)); + return QString(); + } + + // Ask if we should rename + if (renamingMode == "AskEverytime") { + auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); + auto dialog = + CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), + QObject::tr("Would you also like to rename the instance folder?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(oldName, newName), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); + + auto res = dialog->exec(); + if (checkBox->isChecked()) { + if (res == QMessageBox::Yes) + APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); + else + APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); + } + if (res == QMessageBox::No) + return QString(); + } + + // Check for linked instances + if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) + return QString(); + + // Now we can confirm that a renaming is happening + if (!instance->syncInstanceDirName(newRoot)) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("An error occurred when performing the following renaming operation:
" + " - Old instance root: %1
" + " - New instance root: %2
" + "Only the metadata is renamed.") + .arg(oldRoot, newRoot)); + return QString(); + } + return newRoot; +} + +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) +{ + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) { + auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), + QObject::tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "%2 it could break the other instance(s), \n\n" + "Do you wish to proceed?", + nullptr, linkedInstances.count()) + .arg(linkedInstances.join("\n")) + .arg(verb), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return false; + } + return true; +} diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h new file mode 100644 index 000000000..b92a59c4c --- /dev/null +++ b/launcher/InstanceDirUpdate.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * 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 "BaseInstance.h" + +/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened +QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent); + +/// Check if there are linked instances, and display a warning; return true if the operation should proceed +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 57cc77527..e2735385b 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -69,9 +69,10 @@ bool InstanceImportTask::abort() if (!canAbort()) return false; - if (task) - task->abort(); - return Task::abort(); + bool wasAborted = false; + if (m_task) + wasAborted = m_task->abort(); + return wasAborted; } void InstanceImportTask::executeTask() @@ -104,7 +105,7 @@ void InstanceImportTask::downloadFromUrl() connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); - task.reset(filesNetJob); + m_task.reset(filesNetJob); filesNetJob->start(); } @@ -193,7 +194,7 @@ void InstanceImportTask::processZipPack() stepProgress(*progressStep); }); - connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished); + connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection); connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; @@ -210,12 +211,13 @@ void InstanceImportTask::processZipPack() progressStep->status = status; stepProgress(*progressStep); }); - task.reset(zipTask); + m_task.reset(zipTask); zipTask->start(); } void InstanceImportTask::extractFinished() { + setAbortable(false); QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; @@ -289,8 +291,11 @@ void InstanceImportTask::processFlame() inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); - connect(inst_creation_task.get(), &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); + auto weak = inst_creation_task.toWeakRef(); + connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { + if (auto sp = weak.lock()) { + setOverride(sp->shouldOverride(), sp->originalInstanceID()); + } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); @@ -299,11 +304,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::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); - inst_creation_task->start(); + m_task.reset(inst_creation_task); + setAbortable(true); + m_task->start(); } void InstanceImportTask::processTechnic() @@ -350,7 +356,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - ModrinthCreationTask* inst_creation_task = nullptr; + shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); @@ -367,16 +373,16 @@ 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()) { - QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); - pack_id = regex.match(m_sourceUrl.toString()).captured(1); + static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); + pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs - inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id); + inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id); } inst_creation_task->setName(*this); @@ -384,20 +390,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, &InstanceImportTask::emitAborted); + 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 cf86af4ea..8884e0801 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -40,16 +40,13 @@ #include #include "InstanceTask.h" -#include -#include - class QuaZip; class InstanceImportTask : public InstanceTask { Q_OBJECT public: explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); - + virtual ~InstanceImportTask() = default; bool abort() override; protected: @@ -70,7 +67,7 @@ class InstanceImportTask : public InstanceTask { private: /* data */ QUrl m_sourceUrl; QString m_archivePath; - Task::Ptr task; + Task::Ptr m_task; enum class ModpackType { Unknown, MultiMC, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index e1fa755dd..89e7dc04d 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -428,7 +428,7 @@ static QMap getIdMapping(const QList& QList InstanceList::discoverInstances() { - qDebug() << "Discovering instances in" << m_instDir; + qInfo() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { @@ -447,13 +447,9 @@ QList InstanceList::discoverInstances() } auto id = dirInfo.fileName(); out.append(id); - qDebug() << "Found instance ID" << id; + qInfo() << "Found instance ID" << id; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) instanceSet = QSet(out.begin(), out.end()); -#else - instanceSet = out.toSet(); -#endif m_instancesProbed = true; return out; } @@ -468,7 +464,7 @@ InstanceList::InstListError InstanceList::loadList() if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); - qDebug() << "Should keep and soft-reload" << id; + qInfo() << "Should keep and soft-reload" << id; } else { InstancePtr instPtr = loadInstance(id); if (instPtr) { @@ -487,7 +483,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(); diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 174041f89..076341b0b 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -43,11 +43,8 @@ class InstancePageProvider : protected QObject, public BasePageProvider { 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())); - auto logMatcher = inst->getLogFileMatcher(); - if (logMatcher) { - values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); - } + values.append(new InstanceSettingsPage(onesix)); + values.append(new OtherLogsPage(inst)); return values; } diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 3cbf9f9d5..b71000054 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -41,7 +41,9 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { - if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") || + static const QRegularExpression s_memRegex("-Xm[sx]"); + static const QRegularExpression s_versionRegex("-version:.*"); + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " @@ -52,7 +54,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return false; } // block lunacy with passing required version to the JVM - if (jvmargs.contains(QRegularExpression("-version:.*"))) { + if (jvmargs.contains(s_versionRegex)) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" @@ -116,7 +118,7 @@ void JavaCommon::TestCheck::run() emit finished(); return; } - checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0, this)); + checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); checker->start(); } @@ -128,7 +130,7 @@ void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) emit finished(); return; } - checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0, this)); + checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); checker->start(); } diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index a21b5a494..0e4aa2b0a 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -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() = default; void run(); diff --git a/launcher/Json.h b/launcher/Json.h index 28891f398..c13be6470 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -188,10 +188,10 @@ T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = } template -QVector requireIsArrayOf(const QJsonDocument& doc) +QList requireIsArrayOf(const QJsonDocument& doc) { const QJsonArray array = requireArray(doc); - QVector out; + QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } @@ -199,10 +199,10 @@ QVector requireIsArrayOf(const QJsonDocument& doc) } template -QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") +QList ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") { const QJsonArray array = ensureIsType(value, QJsonArray(), what); - QVector out; + QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, what)); } @@ -210,7 +210,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value } template -QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value") +QList ensureIsArrayOf(const QJsonValue& value, const QList default_, const QString& what = "Value") { if (value.isUndefined()) { return default_; @@ -220,7 +220,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, c /// @throw JsonException template -QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") +QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { @@ -230,10 +230,10 @@ QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const } template -QVector ensureIsArrayOf(const QJsonObject& parent, - const QString& key, - const QVector& default_ = QVector(), - const QString& what = "__placeholder__") +QList ensureIsArrayOf(const QJsonObject& parent, + const QString& key, + const QList& default_ = QList(), + const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 687da1322..b1a956b49 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -43,6 +43,7 @@ #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" @@ -61,7 +62,7 @@ #include "launch/steps/TextPrint.h" #include "tasks/Task.h" -LaunchController::LaunchController(QObject* parent) : Task(parent) {} +LaunchController::LaunchController() : Task() {} void LaunchController::executeTask() { @@ -181,7 +182,8 @@ void LaunchController::login() auto name = askOfflineName("Player", m_demo, ok); if (ok) { m_session = std::make_shared(); - m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]"))); + static const QRegularExpression s_removeChars("[{}-]"); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); launchInstance(); return; } @@ -199,8 +201,7 @@ void LaunchController::login() 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()); + m_accountToUse->refresh(); } while (tryagain) { if (tries > 0 && tries % 3 == 0) { @@ -219,13 +220,34 @@ void LaunchController::login() m_session->demo = m_demo; m_accountToUse->fillSession(m_session); - // Launch immediately in true offline mode - if (m_accountToUse->accountType() == AccountType::Offline) { - launchInstance(); + MinecraftAccountPtr accountToCheck; + + if (m_accountToUse->ownsMinecraft()) + accountToCheck = m_accountToUse; + else if (const MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); + defaultAccount != nullptr && defaultAccount->ownsMinecraft()) { + accountToCheck = defaultAccount; + } else { + for (int i = 0; i < APPLICATION->accounts()->count(); i++) { + MinecraftAccountPtr account = APPLICATION->accounts()->at(i); + if (account->ownsMinecraft()) + accountToCheck = account; + } + } + + if (accountToCheck == nullptr) { + if (!m_session->demo) + m_session->demo = askPlayDemo(); + + if (m_session->demo) + launchInstance(); + else + emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + return; } - switch (m_accountToUse->accountState()) { + switch (accountToCheck->accountState()) { case AccountState::Offline: { m_session->wants_online = false; } @@ -234,46 +256,41 @@ void LaunchController::login() if (!m_session->wants_online) { // we ask the user for a player name bool ok = false; - auto name = askOfflineName(m_session->player_name, m_session->demo, ok); - if (!ok) { - tryagain = false; - break; + 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; } m_session->MakeOffline(name); // offline flavored game from here :3 - } - if (m_accountToUse->ownsMinecraft()) { - if (!m_accountToUse->hasProfile()) { - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) { - tryagain = true; - continue; - } else { - emitFailed(tr("Received undetermined session status during login.")); - return; - } - } - // we own Minecraft, there is a profile, it's all ready to go! - launchInstance(); - return; - } else { - // play demo ? - if (!m_session->demo) { - m_session->demo = askPlayDemo(); - } - if (m_session->demo) { // play demo here - launchInstance(); + } else if (m_accountToUse == accountToCheck && !m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) { + tryagain = true; + continue; } else { - emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + emitFailed(tr("Received undetermined session status during login.")); + return; } } + + if (m_accountToUse->accountType() == AccountType::Offline) + m_session->wants_online = false; + + // we own Minecraft, there is a profile, it's all ready to go! + launchInstance(); return; } case AccountState::Errored: // This means some sort of soft error that we can fix with a refresh ... so let's refresh. case AccountState::Unchecked: { - m_accountToUse->refresh(); + accountToCheck->refresh(); } /* fallthrough */ case AccountState::Working: { @@ -282,19 +299,19 @@ void LaunchController::login() if (m_online) { progDialog.setSkipButton(true, tr("Play Offline")); } - auto task = m_accountToUse->currentTask(); + auto task = accountToCheck->currentTask(); progDialog.execWithTask(task.get()); continue; } 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 (reauthenticateAccount(accountToCheck)) + continue; return; } case AccountState::Disabled: { - auto errorString = tr("The launcher's client identification has changed. Please remove this account and add it again."); + auto errorString = tr("The launcher's client identification has changed. Please remove '%1' and try again.") + .arg(accountToCheck->profileName()); + QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); @@ -302,8 +319,9 @@ void LaunchController::login() } case AccountState::Gone: { auto errorString = - tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account " - "you migrated this one to."); + tr("'%1' no longer exists on the servers. It may have been migrated, in which case please add the new account " + "you migrated this one to.") + .arg(accountToCheck->profileName()); QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); @@ -314,6 +332,38 @@ void LaunchController::login() emitFailed(tr("Failed to launch.")); } +bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) +{ + auto button = QMessageBox::warning( + m_parentWidget, tr("Account refresh failed"), + tr("'%1' has expired and needs to be reauthenticated. Do you want to reauthenticate this account?").arg(account->profileName()), + QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); + if (button == QMessageBox::StandardButton::Yes) { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == account; + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); + if (account->accountType() == AccountType::MSA) { + auto newAccount = MSALoginDialog::newAccount(m_parentWidget); + + if (newAccount != nullptr) { + accounts->addAccount(newAccount); + + if (isDefault) + accounts->setDefaultAccount(newAccount); + + if (m_accountToUse == account) { + m_accountToUse = nullptr; + decideAccount(); + } + return true; + } + } + } + + 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"); diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 6e2a94258..af57994f5 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -47,7 +47,7 @@ class LaunchController : public Task { public: void executeTask() override; - LaunchController(QObject* parent = nullptr); + LaunchController(); virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } @@ -56,6 +56,8 @@ 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; } @@ -76,6 +78,7 @@ class LaunchController : public Task { void decideAccount(); bool askPlayDemo(); QString askOfflineName(QString playerName, bool demo, bool& ok); + bool reauthenticateAccount(MinecraftAccountPtr account); private slots: void readyForLaunch(); @@ -87,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; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index dcf3d566f..0b1a2b39e 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -378,7 +378,7 @@ std::optional extractDir(QString fileCompressed, QString dir) if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -395,7 +395,7 @@ std::optional extractDir(QString fileCompressed, QString subdir, QS if (fileInfo.size() == 22) { return QStringList(); } - qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); ; return std::nullopt; } @@ -412,13 +412,13 @@ 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); } -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) @@ -443,8 +443,8 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { - QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); - if (excludeFilter && excludeFilter(relativeFilePath)) { + if (excludeFilter && excludeFilter(e)) { + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); qDebug() << "Skipping file " << relativeFilePath; continue; } @@ -577,7 +577,7 @@ auto ExtractZipTask::extractZip() -> ZipResult auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); auto original_name = relative_file_name; - setStatus("Unziping: " + relative_file_name); + setStatus("Unpacking: " + relative_file_name); // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index d81df9d81..fe0c79de2 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -56,6 +56,7 @@ namespace MMCZip { using FilterFunction = std::function; +using FilterFileFunction = std::function; /** * Merge two zip files, using a filter function @@ -149,7 +150,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir); * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); #if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 116e70c4b..2bd6ecc00 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -2,19 +2,22 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) { - if (levelName == "Launcher") + QString name = levelName.toUpper(); + if (name == "LAUNCHER") return MessageLevel::Launcher; - else if (levelName == "Debug") + else if (name == "TRACE") + return MessageLevel::Trace; + else if (name == "DEBUG") return MessageLevel::Debug; - else if (levelName == "Info") + else if (name == "INFO") return MessageLevel::Info; - else if (levelName == "Message") + else if (name == "MESSAGE") return MessageLevel::Message; - else if (levelName == "Warning") + else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; - else if (levelName == "Error") + else if (name == "ERROR") return MessageLevel::Error; - else if (levelName == "Fatal") + else if (name == "FATAL") return MessageLevel::Fatal; // Skip PrePost, it's not exposed to !![]! // Also skip StdErr and StdOut diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index fd12583f2..321af9d92 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -12,6 +12,7 @@ enum Enum { StdOut, /**< Undetermined stderr messages */ StdErr, /**< Undetermined stdout messages */ Launcher, /**< Launcher Messages */ + Trace, /**< Trace Messages */ Debug, /**< Debug Messages */ Info, /**< Info Messages */ Message, /**< Standard Messages */ diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 3d01c9d33..e603b1634 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -57,8 +57,7 @@ class NullInstance : public BaseInstance { QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } - IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } - QString getLogFileRoot() override { return instanceRoot(); } + QStringList getLogFileSearchPaths() override { return {}; } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } 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/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index 8b28a03f1..5cb3cd0be 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -1,7 +1,6 @@ #include "RecursiveFileSystemWatcher.h" #include -#include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 6f5b9a189..0fe082ac4 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -35,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); } @@ -91,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/StringUtils.cpp b/launcher/StringUtils.cpp index edda9f247..b9e875482 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -53,7 +53,7 @@ static inline QChar getNextChar(const QString& s, int location) int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; - while (l1 <= s1.count() && l2 <= s2.count()) { + while (l1 <= s1.size() && l2 <= s2.size()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) @@ -213,11 +213,10 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular return qMakePair(left, right); } -static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); - QString StringUtils::htmlListPatch(QString htmlStr) { - int pos = htmlStr.indexOf(ulMatcher); + static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); + int pos = htmlStr.indexOf(s_ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index @@ -230,7 +229,7 @@ QString StringUtils::htmlListPatch(QString htmlStr) if (textBetween.isEmpty()) htmlStr.insert(pos, "
"); - pos = htmlStr.indexOf(ulMatcher, pos); + pos = htmlStr.indexOf(s_ulMatcher, pos); } return htmlStr; } \ No newline at end of file diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp index 0dfa74de7..cfcf63805 100644 --- a/launcher/SysInfo.cpp +++ b/launcher/SysInfo.cpp @@ -81,9 +81,9 @@ QString getSupportedJavaArchitecture() if (arch == "arm64") return "mac-os-arm64"; if (arch.contains("64")) - return "mac-os-64"; + return "mac-os-x64"; if (arch.contains("86")) - return "mac-os-86"; + return "mac-os-x86"; // Unknown, maybe something new, appending arch return "mac-os-" + arch; } else if (sys == "linux") { diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 2edb17e72..bffe5d58a 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,7 +1,6 @@ #include "Version.h" #include -#include #include #include @@ -79,7 +78,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()) diff --git a/launcher/Version.h b/launcher/Version.h index b06e256aa..12e7f0832 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -72,22 +72,14 @@ class Version { } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto numPart = QStringView{ m_fullString }.left(cutoff); -#else - auto numPart = m_fullString.leftRef(cutoff); -#endif if (!numPart.isEmpty()) { m_isNull = false; m_numPart = numPart.toInt(); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto stringPart = QStringView{ m_fullString }.mid(cutoff); -#else - auto stringPart = m_fullString.midRef(cutoff); -#endif if (!stringPart.isEmpty()) { m_isNull = false; diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 12a82f73d..165dd4cb7 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -193,8 +193,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const if (value.toBool()) { return tr("Recommended"); } else if (hasLatest) { - auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (value.toBool()) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { return tr("Latest"); } } @@ -203,33 +203,27 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const } } case Qt::DecorationRole: { - switch (column) { - case Name: { - if (hasRecommended) { - auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); - if (recommenced.toBool()) { - return APPLICATION->getThemedIcon("star"); - } else if (hasLatest) { - auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (latest.toBool()) { - return APPLICATION->getThemedIcon("bug"); - } - } - QPixmap pixmap; - QPixmapCache::find("placeholder", &pixmap); - if (!pixmap) { - QPixmap px(16, 16); - px.fill(Qt::transparent); - QPixmapCache::insert("placeholder", px); - return px; - } - return pixmap; + if (column == Name && hasRecommended) { + auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (recommenced.toBool()) { + return APPLICATION->getThemedIcon("star"); + } else if (hasLatest) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { + return APPLICATION->getThemedIcon("bug"); } } - default: { - return QVariant(); + QPixmap pixmap; + QPixmapCache::find("placeholder", &pixmap); + if (!pixmap) { + QPixmap px(16, 16); + px.fill(Qt::transparent); + QPixmapCache::insert("placeholder", px); + return px; } + return pixmap; } + return QVariant(); } default: { if (roles.contains((BaseVersionList::ModelRoles)role)) { @@ -301,7 +295,6 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); - beginResetModel(); m_columns.clear(); if (!replacing) { @@ -348,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) hasLatest = true; } filterModel->setSourceModel(replacing); - - endResetModel(); } QModelIndex VersionProxyModel::getRecommended() const diff --git a/launcher/console/Console.h b/launcher/console/Console.h new file mode 100644 index 000000000..7aaf83dcc --- /dev/null +++ b/launcher/console/Console.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#endif + +namespace console { + +inline bool isConsole() +{ +#if defined Q_OS_WIN32 + DWORD procIDs[2]; + DWORD maxCount = 2; + DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount); + return result > 1; +#else + if (isatty(fileno(stdout))) { + return true; + } + return false; +#endif +} + +} // namespace console diff --git a/launcher/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp similarity index 78% rename from launcher/WindowsConsole.cpp rename to launcher/console/WindowsConsole.cpp index 83cad5afa..4a0eb3d3d 100644 --- a/launcher/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -16,13 +16,18 @@ * */ +#include "WindowsConsole.h" +#include + #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif +#include + #include #include #include -#include +#include #include void RedirectHandle(DWORD handle, FILE* stream, const char* mode) @@ -126,3 +131,29 @@ bool AttachWindowsConsole() return false; } + +std::error_code EnableAnsiSupport() +{ + // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected + HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0); + if (console_handle == INVALID_HANDLE_VALUE) { + return std::error_code(GetLastError(), std::system_category()); + } + + // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode + DWORD console_mode; + if (0 == GetConsoleMode(console_handle, &console_mode)) { + return std::error_code(GetLastError(), std::system_category()); + } + + // VT processing not already enabled? + if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) { + // https://docs.microsoft.com/en-us/windows/console/setconsolemode + if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { + return std::error_code(GetLastError(), std::system_category()); + } + } + + return {}; +} diff --git a/launcher/WindowsConsole.h b/launcher/console/WindowsConsole.h similarity index 93% rename from launcher/WindowsConsole.h rename to launcher/console/WindowsConsole.h index ab53864b4..4c1f3ee28 100644 --- a/launcher/WindowsConsole.h +++ b/launcher/console/WindowsConsole.h @@ -21,5 +21,8 @@ #pragma once +#include + void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); +std::error_code EnableAnsiSupport(); diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index bdf173ebc..1494fa8cc 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -37,27 +37,14 @@ #include #if defined Q_OS_WIN32 -#include "WindowsConsole.h" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include "console/WindowsConsole.h" #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { @@ -104,11 +91,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 +119,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 e4157ea2d..8a2a482e1 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -37,7 +37,6 @@ #include "IconList.h" #include #include -#include #include #include #include @@ -47,24 +46,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 +76,129 @@ 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) +{ + QSet set(list.begin(), list.end()); + 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); + QFileInfo icon(it.getFilePath()); + currentSet.insert(icon.absoluteFilePath()); } -#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 relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); 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 +208,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 +237,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 +249,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 +279,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 +300,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,12 +349,13 @@ 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) @@ -332,22 +370,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 +397,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 +427,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 +462,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(); +} \ No newline at end of file diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index 553946c42..d2f904448 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -51,7 +51,7 @@ class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: - explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); + explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); virtual ~IconList() {}; QIcon getIcon(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; + QList m_icons; QDir m_dir; }; diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index c54a5b04b..0aa725705 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -44,8 +44,8 @@ #include "FileSystem.h" #include "java/JavaUtils.h" -JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id, QObject* parent) - : Task(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) +JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id) + : Task(), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) {} void JavaChecker::executeTask() @@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QMap results; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); -#else - QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); -#endif for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux @@ -149,11 +145,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) continue; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto parts = line.split('=', Qt::SkipEmptyParts); -#else - auto parts = line.split('=', QString::SkipEmptyParts); -#endif if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { @@ -171,7 +163,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) 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 = Result::Validity::Valid; result.is_64bit = is_64; diff --git a/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h index 171a18b76..a04b68170 100644 --- a/launcher/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include "JavaVersion.h" #include "QObjectPtr.h" @@ -26,7 +25,7 @@ class JavaChecker : public Task { enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; }; - explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0, QObject* parent = 0); + explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0); signals: void checkFinished(const Result& result); diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 569fda306..aa7fab8a0 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -163,7 +163,7 @@ void JavaListLoadTask::executeTask() JavaUtils ju; QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); - ConcurrentTask::Ptr job(new ConcurrentTask(this, "Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + ConcurrentTask::Ptr job(new ConcurrentTask("Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); m_job.reset(job); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); @@ -171,7 +171,7 @@ void JavaListLoadTask::executeTask() qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { - auto checker = new JavaChecker(candidate, "", 0, 0, 0, id, this); + auto checker = new JavaChecker(candidate, "", 0, 0, 0, id); connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); job->addTask(Task::Ptr(checker)); id++; diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index bc8026348..072cb1d16 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -102,6 +102,8 @@ QProcessEnvironment CleanEnviroment() QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); qDebug() << "Env: stripped" << key << value << "to" << newValue; + + value = newValue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus @@ -403,7 +405,7 @@ QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); - auto scanJavaDir = [&]( + auto scanJavaDir = [&javas]( const QString& dirPath, const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); @@ -422,7 +424,7 @@ 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); @@ -440,9 +442,15 @@ QList JavaUtils::FindJavaPaths() 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 @@ -544,12 +552,12 @@ QStringList getPrismJavaBundle() { QList javas; - auto scanDir = [&](QString prefix) { + 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 = [&](const QString& dirPath) { + auto scanJavaDir = [scanDir](const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index bca50f2c9..e9a160ea7 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString) QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { - pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withOne( + "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withOne; } else { - pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withoutOne( + "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withoutOne; } auto match = pattern.match(m_string); diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index bb7cc568d..bb31ca1e2 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -55,6 +55,7 @@ void ArchiveDownloadTask::executeTask() 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::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); @@ -135,7 +136,6 @@ 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/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 836afeaac..20b39e751 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { - // this is linux only ! + // this is *nix only ! auto path = Json::ensureString(meta, "target"); if (!path.isEmpty()) { - auto target = FS::PathCombine(file, "../" + path); - QFile(target).link(file); + QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? diff --git a/launcher/launch/LaunchStep.cpp b/launcher/launch/LaunchStep.cpp index f3e9dfce0..0b352ea9f 100644 --- a/launcher/launch/LaunchStep.cpp +++ b/launcher/launch/LaunchStep.cpp @@ -16,7 +16,7 @@ #include "LaunchStep.h" #include "LaunchTask.h" -LaunchStep::LaunchStep(LaunchTask* parent) : Task(parent), m_parent(parent) +LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent) { connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 0251b302d..b67df7631 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,12 +37,12 @@ #include "launch/LaunchTask.h" #include +#include #include #include #include -#include -#include #include +#include #include "MessageLevel.h" #include "tasks/Task.h" @@ -214,6 +214,52 @@ shared_qobject_ptr LaunchTask::getLogModel() return m_logModel; } +bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level) +{ + LogParser* parser; + switch (level) { + case MessageLevel::StdErr: + parser = &m_stderrParser; + break; + case MessageLevel::StdOut: + parser = &m_stdoutParser; + break; + default: + return false; + } + + parser->appendLine(line); + auto items = parser->parseAvailable(); + if (auto err = parser->getError(); err.has_value()) { + auto& model = *getLogModel(); + model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); + return false; + } else { + if (!items.isEmpty()) { + auto& model = *getLogModel(); + for (auto const& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + msg = censorPrivateInfo(msg); + model.append(entry.level, msg); + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + level = LogParser::guessLevel(msg, model.previousLevel()); + msg = censorPrivateInfo(msg); + model.append(level, msg); + } + } + } + } + return true; +} + void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel) { for (auto& line : lines) { @@ -223,21 +269,26 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) { + if (parseXmlLogs(line, level)) { + return; + } + // if the launcher part set a log level, use it auto innerLevel = MessageLevel::fromLine(line); if (innerLevel != MessageLevel::Unknown) { level = innerLevel; } + auto& model = *getLogModel(); + // If the level is still undetermined, guess level - if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { - level = m_instance->guessLevel(line, level); + if (level == MessageLevel::Unknown) { + level = LogParser::guessLevel(line, model.previousLevel()); } // censor private user info line = censorPrivateInfo(line); - auto& model = *getLogModel(); model.append(level, line); } @@ -254,20 +305,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 56065af5b..5effab980 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -43,6 +43,7 @@ #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" +#include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT @@ -87,8 +88,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 */ @@ -115,6 +115,9 @@ class LaunchTask : public Task { private: /*methods */ void finalizeSteps(bool successful, const QString& error); + protected: + bool parseXmlLogs(QString const& line, MessageLevel::Enum level); + protected: /* data */ MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; @@ -123,4 +126,6 @@ class LaunchTask : public Task { int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; + LogParser m_stdoutParser; + LogParser m_stderrParser; }; diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 23a33ae18..90af9787d 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines) return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary - QVector newContent; + QList newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over @@ -149,3 +149,28 @@ bool LogModel::wrapLines() const { return m_lineWrap; } + +void LogModel::setColorLines(bool state) +{ + if (m_colorLines != state) { + m_colorLines = state; + } +} + +bool LogModel::colorLines() const +{ + return m_colorLines; +} + +bool LogModel::isOverFlow() +{ + return m_numLines >= m_maxLines && m_stopOnOverflow; +} + +MessageLevel::Enum LogModel::previousLevel() +{ + if (!m_content.isEmpty()) { + return m_content.last().level; + } + return MessageLevel::Unknown; +} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 18e51d7e3..4521bac17 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -24,20 +24,25 @@ class LogModel : public QAbstractListModel { void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); + bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; + void setColorLines(bool state); + bool colorLines() const; + + MessageLevel::Enum previousLevel(); enum Roles { LevelRole = Qt::UserRole }; private /* types */: struct entry { - MessageLevel::Enum level; + MessageLevel::Enum level = MessageLevel::Enum::Unknown; QString line; }; private: /* data */ - QVector m_content; + QList m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; @@ -47,6 +52,7 @@ class LogModel : public QAbstractListModel { QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; + bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 55d13b58c..0f8d27e94 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -94,7 +94,7 @@ void CheckJava::executeTask() // if timestamps are not the same, or something is missing, check! if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { - m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0, this)); + m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->start(); diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 725101224..6b960974e 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -47,25 +47,17 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PostLaunchCommand::executeTask() { - // FIXME: where to put this? -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - auto args = QProcess::splitCommand(m_command); - m_parent->substituteVariables(args); + auto cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); + 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); -#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/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 6d071a66e..7e843ca3f 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -47,25 +47,16 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PreLaunchCommand::executeTask() { - // FIXME: where to put this? -#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 cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), 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); -#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/logs/LogParser.cpp b/launcher/logs/LogParser.cpp new file mode 100644 index 000000000..0790dec4d --- /dev/null +++ b/launcher/logs/LogParser.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 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 "LogParser.h" + +#include +#include "MessageLevel.h" + +using namespace Qt::Literals::StringLiterals; + +void LogParser::appendLine(QAnyStringView data) +{ + if (!m_partialData.isEmpty()) { + m_buffer = QString(m_partialData); + m_buffer.append("\n"); + m_partialData.clear(); + } + m_buffer.append(data.toString()); +} + +std::optional LogParser::getError() +{ + return m_error; +} + +std::optional LogParser::parseAttributes() +{ + LogParser::LogEntry entry{ + "", + MessageLevel::Info, + }; + auto attributes = m_parser.attributes(); + + for (const auto& attr : attributes) { + auto name = attr.name(); + auto value = attr.value(); + if (name == "logger"_L1) { + entry.logger = value.trimmed().toString(); + } else if (name == "timestamp"_L1) { + if (value.trimmed().isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); + return {}; + } + entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); + } else if (name == "level"_L1) { + entry.levelText = value.trimmed().toString(); + entry.level = MessageLevel::getLevel(entry.levelText); + } else if (name == "thread"_L1) { + entry.thread = value.trimmed().toString(); + } + } + if (entry.logger.isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: logger"); + return {}; + } + + return entry; +} + +void LogParser::setError() +{ + m_error = { + m_parser.errorString(), + m_parser.error(), + }; +} + +void LogParser::clearError() +{ + m_error = {}; // clear previous error +} + +bool isPotentialLog4JStart(QStringView buffer) +{ + static QString target = QStringLiteral(" LogParser::parseNext() +{ + clearError(); + + if (m_buffer.isEmpty()) { + return {}; + } + + if (m_buffer.trimmed().isEmpty()) { + auto text = QString(m_buffer); + m_buffer.clear(); + return LogParser::PlainText{ text }; + } + + // check if we have a full xml log4j event + bool isCompleteLog4j = false; + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + if (m_parser.readNextStartElement()) { + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + int depth = 1; + bool eod = false; + while (depth > 0 && !eod) { + auto tok = m_parser.readNext(); + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + depth += 1; + } break; + case QXmlStreamReader::TokenType::EndElement: { + depth -= 1; + } break; + case QXmlStreamReader::TokenType::EndDocument: { + eod = true; // break outer while loop + } break; + default: { + // no op + } + } + if (m_parser.hasError()) { + break; + } + } + + isCompleteLog4j = depth == 0; + } + } + + if (isCompleteLog4j) { + return parseLog4J(); + } else { + if (isPotentialLog4JStart(m_buffer)) { + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + + int start = 0; + auto bufView = QStringView(m_buffer); + while (start < bufView.length()) { + if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { + auto slicestart = start + pos; + auto slice = bufView.right(bufView.length() - slicestart); + if (isPotentialLog4JStart(slice)) { + if (slicestart > 0) { + auto text = m_buffer.left(slicestart); + m_buffer = m_buffer.right(m_buffer.length() - slicestart); + if (!text.trimmed().isEmpty()) { + return LogParser::PlainText{ text }; + } + } + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + start = slicestart + 1; + } else { + break; + } + } + + // no log4j found, all plain text + auto text = QString(m_buffer); + m_buffer.clear(); + return LogParser::PlainText{ text }; + } +} + +QList LogParser::parseAvailable() +{ + QList items; + bool doNext = true; + while (doNext) { + auto item_ = parseNext(); + if (m_error.has_value()) { + return {}; + } + if (item_.has_value()) { + auto item = item_.value(); + if (std::holds_alternative(item)) { + break; + } else { + items.push_back(item); + } + } else { + doNext = false; + } + } + return items; +} + +std::optional LogParser::parseLog4J() +{ + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + + m_parser.readNextStartElement(); + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + auto entry_ = parseAttributes(); + if (!entry_.has_value()) { + setError(); + return {}; + } + auto entry = entry_.value(); + + bool foundMessage = false; + int depth = 1; + + enum parseOp { noOp, entryReady, parseError }; + + auto foundStart = [&]() -> parseOp { + depth += 1; + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + QString message; + bool messageComplete = false; + + while (!messageComplete) { + auto tok = m_parser.readNext(); + + switch (tok) { + case QXmlStreamReader::TokenType::Characters: { + message.append(m_parser.text()); + } break; + case QXmlStreamReader::TokenType::EndElement: { + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + messageComplete = true; + } + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return parseError; // parse fail + } break; + default: { + // no op + } + } + + if (m_parser.hasError()) { + return parseError; + } + } + + entry.message = message; + foundMessage = true; + depth -= 1; + } + return noOp; + }; + + auto foundEnd = [&]() -> parseOp { + depth -= 1; + if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + if (foundMessage) { + auto consumed = m_parser.characterOffset(); + if (consumed > 0 && consumed <= m_buffer.length()) { + m_buffer = m_buffer.right(m_buffer.length() - consumed); + // potential whitespace preserved for next item + } + clearError(); + return entryReady; + } + m_parser.raiseError("log4j:Event Missing required attribute: message"); + setError(); + return parseError; + } + return noOp; + }; + + while (!m_parser.atEnd()) { + auto tok = m_parser.readNext(); + parseOp op = noOp; + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + op = foundStart(); + } break; + case QXmlStreamReader::TokenType::EndElement: { + op = foundEnd(); + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return {}; + } break; + default: { + // no op + } + } + + switch (op) { + case parseError: + return {}; // parse fail or error + case entryReady: + return entry; + case noOp: + default: { + // no op + } + } + + if (m_parser.hasError()) { + return {}; + } + } + } + + throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); +} + +MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level) +{ + static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = LINE_WITH_LEVEL.match(line); + if (match.hasMatch()) { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + level = MessageLevel::getLevel(levelStr); + } else { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || + line.contains("[FINEST]")) + level = MessageLevel::Info; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (level != MessageLevel::Unknown) + return level; + + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + + return MessageLevel::Info; +} diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h new file mode 100644 index 000000000..1a1d86dd1 --- /dev/null +++ b/launcher/logs/LogParser.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 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 +#include +#include +#include +#include +#include +#include +#include +#include "MessageLevel.h" + +class LogParser { + public: + struct LogEntry { + QString logger; + MessageLevel::Enum level; + QString levelText; + QDateTime timestamp; + QString thread; + QString message; + }; + struct Partial { + QString data; + }; + struct PlainText { + QString message; + }; + struct Error { + QString errMessage; + QXmlStreamReader::Error error; + }; + + using ParsedItem = std::variant; + + public: + LogParser() = default; + + void appendLine(QAnyStringView data); + std::optional parseNext(); + QList parseAvailable(); + std::optional getError(); + + /// guess log level from a line of game log + static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level); + + protected: + std::optional parseAttributes(); + void setError(); + void clearError(); + + std::optional parseLog4J(); + + private: + QString m_buffer; + QString m_partialData; + QXmlStreamReader m_parser; + std::optional m_error; +}; diff --git a/launcher/main.cpp b/launcher/main.cpp index 35f545f52..c41c510dd 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -84,6 +84,8 @@ int main(int argc, char* argv[]) Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); + + Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index bd0745b6b..25a4cd146 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -23,7 +23,7 @@ namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} -Index::Index(const QVector& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) +Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); @@ -103,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QVector lists = other->m_lists; + const QList lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); @@ -140,8 +140,8 @@ Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mo } auto versionList = get(uid); - auto loadTask = makeShared( - this, tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); + auto loadTask = + makeShared(tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); if (status() != BaseEntity::LoadStatus::Remote || force) { loadTask->addTask(this->loadTask(mode)); } diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index 026a00c07..fe5bf2170 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); - explicit Index(const QVector& lists, QObject* parent = nullptr); + explicit Index(const QList& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -46,7 +46,7 @@ class Index : public QAbstractListModel, public BaseEntity { Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; - QVector lists() const { return m_lists; } + QList lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); @@ -60,7 +60,7 @@ class Index : public QAbstractListModel, public BaseEntity { void parse(const QJsonObject& obj) override; private: - QVector m_lists; + QList m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 86af7277e..8d8466c87 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -35,8 +35,8 @@ MetadataVersion currentFormatVersion() // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { - const QVector objects = requireIsArrayOf(obj, "packages"); - QVector lists; + const QList objects = requireIsArrayOf(obj, "packages"); + QList lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); @@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); - const QVector versionsRaw = requireIsArrayOf(obj, "versions"); - QVector versions; + const QList versionsRaw = requireIsArrayOf(obj, "versions"); + QList versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 46dc740da..2327879a1 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -19,8 +19,8 @@ #include "BaseVersion.h" #include +#include #include -#include #include #include "minecraft/VersionFile.h" diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 6856b5f8d..1f4a969fa 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -34,8 +34,7 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( Task::Ptr VersionList::getLoadTask() { - auto loadTask = - makeShared(this, tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); + auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); loadTask->addTask(this->loadTask(Net::Mode::Online)); return loadTask; @@ -159,8 +158,8 @@ Version::Ptr VersionList::getVersion(const QString& version) 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()); } @@ -170,7 +169,7 @@ void VersionList::setName(const QString& name) emit nameChanged(name); } -void VersionList::setVersions(const QVector& versions) +void VersionList::setVersions(const QList& versions) { beginResetModel(); m_versions = versions; @@ -266,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); + [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [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 }); }); diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 4215439db..21c86b751 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity { Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; - QVector versions() const { return m_versions; } + QList 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 setVersions(const QList& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; @@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity { void updateListData(QList) override {} private: - QVector m_versions; + QList m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 1073ef324..ad7ef545c 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -222,11 +222,12 @@ bool Component::isMoveable() return true; } -bool Component::isVersionChangeable() +bool Component::isVersionChangeable(bool wait) { auto list = getVersionList(); if (list) { - list->waitToLoad(); + if (wait) + list->waitToLoad(); return list->count() != 0; } return false; diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index 7ff30889f..203cc2241 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -72,7 +72,7 @@ class Component : public QObject, public ProblemProvider { bool isRevertible(); bool isRemovable(); bool isCustom(); - bool isVersionChangeable(); + bool isVersionChangeable(bool wait = true); bool isKnownModloader(); QStringList knownConflictingComponents(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 6656a84f8..36a07ee72 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -38,7 +38,7 @@ * 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_profile = list; diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 484c0bedd..64c55877b 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -14,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: diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 22db7d641..a2588064f 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -54,11 +54,11 @@ struct GradleSpecifier { 4 "jdk15" 5 "jar" */ - QRegularExpression matcher( + static const QRegularExpression s_matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); - QRegularExpressionMatch match = matcher.match(value); + QRegularExpressionMatch match = s_matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 4f04f0eb9..0bc462474 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -65,7 +65,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, { bool local = isLocal(); // Lambda function to get the absolute file path - auto actualPath = [&](QString relPath) { + auto actualPath = [this, local, overridePath](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { @@ -115,7 +115,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC bool local = isLocal(); // Lambda function to check if a local file exists - auto check_local_file = [&](QString storage) { + auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); @@ -128,7 +128,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC }; // Lambda function to add a download request - auto add_download = [&](QString storage, QString url, QString sha1) { + auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); } @@ -198,7 +198,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC } } } else { - auto raw_dl = [&]() { + auto raw_dl = [this, raw_storage]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 98ac7647f..991afce89 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -53,7 +53,6 @@ #include "MMCTime.h" #include "java/JavaVersion.h" #include "pathmatcher/MultiMatcher.h" -#include "pathmatcher/RegexpMatcher.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" @@ -91,13 +90,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 { @@ -530,7 +572,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"; @@ -551,6 +593,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; } @@ -606,6 +655,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() // dlsym variant is only needed for OpenGL and not included in the vulkan layer appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_opengl.so"); + appendLib("libMangoHud_shim.so"); preloadList << mangoHudLibString; } @@ -614,12 +664,14 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() } 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()) { @@ -636,9 +688,9 @@ static QString replaceTokensIn(QString text, QMap with) { // TODO: does this still work?? QString result; - QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); QStringList list; - QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); + QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); int lastCapturedEnd = 0; while (i.hasNext()) { QRegularExpressionMatch match = i.next(); @@ -704,11 +756,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine token_mapping["assets_root"] = absAssetsDir; token_mapping["assets_index_name"] = assets->id; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); -#else - QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); -#endif for (int i = 0; i < parts.length(); i++) { parts[i] = replaceTokensIn(parts[i], token_mapping); } @@ -752,11 +800,30 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT // 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(); + screenGeometry = screenGeometry.shrunkBy(m); + 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"; } @@ -828,7 +895,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; @@ -848,7 +915,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr } // mods and core mods - auto printModList = [&](const QString& label, ModFolderModel& model) { + auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); @@ -936,61 +1003,9 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess return filter; } -MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) +QStringList MinecraftInstance::getLogFileSearchPaths() { - QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - auto match = re.match(line); - if (match.hasMatch()) { - // New style logs from log4j - QString timestamp = match.captured("timestamp"); - QString levelStr = match.captured("level"); - if (levelStr == "INFO") - level = MessageLevel::Message; - if (levelStr == "WARN") - level = MessageLevel::Warning; - if (levelStr == "ERROR") - level = MessageLevel::Error; - if (levelStr == "FATAL") - level = MessageLevel::Fatal; - if (levelStr == "TRACE" || levelStr == "DEBUG") - level = MessageLevel::Debug; - } else { - // Old style forge logs - if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || - line.contains("[FINEST]")) - level = MessageLevel::Message; - if (line.contains("[SEVERE]") || line.contains("[STDERR]")) - level = MessageLevel::Error; - if (line.contains("[WARNING]")) - level = MessageLevel::Warning; - if (line.contains("[DEBUG]")) - level = MessageLevel::Debug; - } - if (line.contains("overwriting existing")) - return MessageLevel::Fatal; - // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * - static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; - if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at " + javaSymbol)) || - line.contains(QRegularExpression("Caused by: " + javaSymbol)) || - line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) || - line.contains(QRegularExpression("... \\d+ more$"))) - return MessageLevel::Error; - return level; -} - -IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() -{ - auto combined = std::make_shared(); - combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); - combined->add(std::make_shared("crash-.*\\.txt")); - combined->add(std::make_shared("IDMap dump.*\\.txt$")); - combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); - return combined; -} - -QString MinecraftInstance::getLogFileRoot() -{ - return gameRoot(); + return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() @@ -1083,17 +1098,10 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } - // run pre-launch command if that's needed - if (getPreLaunchCommand().size()) { - auto step = makeShared(pptr); - step->setWorkingDirectory(gameRoot()); - process->appendStep(step); - } - // load meta { auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; - process->appendStep(makeShared(pptr, makeShared(this, mode, pptr))); + process->appendStep(makeShared(pptr, makeShared(this, mode))); } // check java @@ -1102,6 +1110,13 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } + // run pre-launch command if that's needed + if (getPreLaunchCommand().size()) { + auto step = makeShared(pptr); + step->setWorkingDirectory(gameRoot()); + process->appendStep(step); + } + // if we aren't in offline mode,. if (session->status != AuthSession::PlayableOffline) { if (!session->demo) { @@ -1177,7 +1192,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; } @@ -1186,7 +1201,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; } @@ -1203,7 +1218,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; } @@ -1211,7 +1227,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; } @@ -1219,11 +1236,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 75e97ae45..5790e0619 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -116,6 +116,7 @@ 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(); @@ -138,12 +139,7 @@ class MinecraftInstance : public BaseInstance { QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; - /// guess log level from a line of minecraft log - MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override; - - IPathMatcher::Ptr getLogFileMatcher() override; - - QString getLogFileRoot() override; + QStringList getLogFileSearchPaths() override; QString getStatusbarDescription() override; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index a9dcdf067..c0a82e61e 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -2,15 +2,16 @@ #include "MinecraftInstance.h" #include "PackProfile.h" -MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode, QObject* parent) - : Task(parent), m_inst(inst), m_netmode(netmode) -{} +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(m_netmode); + if (auto result = components->reload(m_netmode); !result) { + emitFailed(result.error); + return; + } m_task = components->getCurrentTask(); if (!m_task) { diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h index 72e9e0caa..c05698bca 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.h +++ b/launcher/minecraft/MinecraftLoadAndCheck.h @@ -23,7 +23,7 @@ class MinecraftInstance; class MinecraftLoadAndCheck : public Task { Q_OBJECT public: - explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode, QObject* parent = nullptr); + explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); virtual ~MinecraftLoadAndCheck() = default; void executeTask() override; diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index bd587beb2..32dd1875c 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->uid = root.value("fileId").toString(); } - const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern( + static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; - if (!valid_uid_regex.match(out->uid).hasMatch()) { + if (!s_validUidRegex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); @@ -176,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 diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index f1d2473c2..8475a1f32 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -173,29 +173,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c } // 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()) { - qCWarning(instanceProfileC) << "Components file" << filename << "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)) { - qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); + 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 false; + return PackProfile::Result::Error(message); } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { - qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); + 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 false; + return PackProfile::Result::Error(message); } // and then read it and process it if all above is true. @@ -212,11 +215,13 @@ static bool loadPackProfile(PackProfile* parent, container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] const JSONValidationError& err) { - qCCritical(instanceProfileC) << "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 @@ -283,44 +288,43 @@ void PackProfile::save_internal() 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)) { + if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) { qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; - 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() << 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 true; + 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 @@ -329,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() @@ -511,13 +517,9 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const switch (role) { case Qt::CheckStateRole: { - switch (column) { - case NameColumn: { - return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; - } - default: - return QVariant(); - } + if (column == NameColumn) + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); } case Qt::DisplayRole: { switch (column) { @@ -643,11 +645,7 @@ void PackProfile::move(const int index, const MoveDirection direction) return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) d->components.swapItemsAt(index, theirIndex); -#else - d->components.swap(index, theirIndex); -#endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); @@ -746,7 +744,7 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch) } // 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; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index b2de26ea0..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); @@ -169,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/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index 08ec0fac3..a79f89529 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -41,7 +41,6 @@ #include #include -#include #include namespace ProfileUtils { diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index bd28f9e9a..8ae097bad 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -198,22 +198,6 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) return f.commit(); } -int64_t calculateWorldSize(const QFileInfo& file) -{ - if (file.isFile() && file.suffix() == "zip") { - return file.size(); - } else if (file.isDir()) { - QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); - int64_t total = 0; - while (it.hasNext()) { - it.next(); - total += it.fileInfo().size(); - } - return total; - } - return -1; -} - World::World(const QFileInfo& file) { repath(file); @@ -223,7 +207,6 @@ void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); - m_size = calculateWorldSize(file); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); @@ -252,41 +235,41 @@ void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { - is_valid = false; + m_isValid = false; return; } loadFromLevelDat(bytes); - levelDatTime = file.lastModified(); + m_levelDatTime = file.lastModified(); } void World::readFromZip(const QFileInfo& file) { QuaZip zip(file.absoluteFilePath()); - is_valid = zip.open(QuaZip::mdUnzip); - if (!is_valid) { + m_isValid = zip.open(QuaZip::mdUnzip); + if (!m_isValid) { return; } auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); - is_valid = !location.isEmpty(); - if (!is_valid) { + m_isValid = !location.isEmpty(); + if (!m_isValid) { return; } m_containerOffsetPath = location; QuaZipFile zippedFile(&zip); // read the install profile - is_valid = zip.setCurrentFile(location + "level.dat"); - if (!is_valid) { + m_isValid = zip.setCurrentFile(location + "level.dat"); + if (!m_isValid) { return; } - is_valid = zippedFile.open(QIODevice::ReadOnly); + m_isValid = zippedFile.open(QIODevice::ReadOnly); QuaZipFileInfo64 levelDatInfo; zippedFile.getFileInfo(&levelDatInfo); auto modTime = levelDatInfo.getNTFSmTime(); if (!modTime.isValid()) { modTime = levelDatInfo.dateTime; } - levelDatTime = modTime; - if (!is_valid) { + m_levelDatTime = modTime; + if (!m_isValid) { return; } loadFromLevelDat(zippedFile.readAll()); @@ -430,7 +413,7 @@ void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { - is_valid = false; + m_isValid = false; return; } @@ -439,20 +422,20 @@ void World::loadFromLevelDat(QByteArray data) valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); - is_valid = false; + m_isValid = false; return; } nbt::value& val = *valPtr; - is_valid = val.get_type() == nbt::tag_type::Compound; - if (!is_valid) + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); - m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; m_gameType = read_gametype(val, "GameType"); @@ -490,7 +473,7 @@ bool World::replace(World& with) bool World::destroy() { - if (!is_valid) + if (!m_isValid) return false; if (FS::trash(m_containerFile.filePath())) @@ -508,7 +491,7 @@ bool World::destroy() bool World::operator==(const World& other) const { - return is_valid == other.is_valid && folderName() == other.folderName(); + return m_isValid == other.m_isValid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const @@ -531,3 +514,8 @@ bool World::isMoreThanOneHardLink() const } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } + +void World::setSize(int64_t size) +{ + m_size = size; +} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 4303dc553..34d418e79 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -39,7 +39,7 @@ class World { QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } - bool isValid() const { return is_valid; } + bool isValid() const { return m_isValid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world @@ -54,6 +54,8 @@ class World { bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); + void setSize(int64_t size); + // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; @@ -83,10 +85,10 @@ class World { QString m_folderName; QString m_actualName; QString m_iconFile; - QDateTime levelDatTime; + QDateTime m_levelDatTime; QDateTime m_lastPlayed; int64_t m_size; int64_t m_randomSeed = 0; GameType m_gameType; - bool is_valid = false; + bool m_isValid = false; }; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 812b13c71..6a821ba60 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -37,13 +37,14 @@ #include #include +#include #include #include #include +#include #include #include #include -#include "Application.h" WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { @@ -51,18 +52,18 @@ WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractList m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); - is_watching = false; + m_isWatching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -71,11 +72,11 @@ void WorldList::startWatching() void WorldList::stopWatching() { - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -101,12 +102,13 @@ bool WorldList::update() } } beginResetModel(); - worlds.swap(newWorlds); + m_worlds.swap(newWorlds); endResetModel(); + loadWorldsAsync(); return true; } -void WorldList::directoryChanged(QString path) +void WorldList::directoryChanged(QString) { update(); } @@ -123,12 +125,12 @@ QString WorldList::instDirPath() const bool WorldList::deleteWorld(int index) { - if (index >= worlds.size() || index < 0) + if (index >= m_worlds.size() || index < 0) return false; - World& m = worlds[index]; + World& m = m_worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); - worlds.removeAt(index); + m_worlds.removeAt(index); endRemoveRows(); emit changed(); return true; @@ -139,11 +141,11 @@ bool WorldList::deleteWorld(int index) bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { - World& m = worlds[i]; + World& m = m_worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); - worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; @@ -151,9 +153,9 @@ bool WorldList::deleteWorlds(int first, int last) bool WorldList::resetIcon(int row) { - if (row >= worlds.size() || row < 0) + if (row >= m_worlds.size() || row < 0) return false; - World& m = worlds[row]; + World& m = m_worlds[row]; if (m.resetIcon()) { emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); return true; @@ -174,12 +176,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= worlds.size()) + if (row < 0 || row >= m_worlds.size()) return QVariant(); QLocale locale; - auto& world = worlds[row]; + auto& world = m_worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { @@ -208,13 +210,9 @@ QVariant WorldList::data(const QModelIndex& index, int role) const } case Qt::UserRole: - switch (column) { - case SizeColumn: - return QVariant::fromValue(world.bytes()); - - default: - return data(index, Qt::DisplayRole); - } + if (column == SizeColumn) + return QVariant::fromValue(world.bytes()); + return data(index, Qt::DisplayRole); case Qt::ToolTipRole: { if (column == InfoColumn) { @@ -311,11 +309,7 @@ class WorldMimeData : public QMimeData { QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } protected: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QVariant retrieveData(const QString& mimetype, QMetaType type) const -#else - QVariant retrieveData(const QString& mimetype, QVariant::Type type) const -#endif { QList urls; for (auto& world : m_worlds) { @@ -343,9 +337,9 @@ QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const if (idx.column() != 0) continue; int row = idx.row(); - if (row < 0 || row >= this->worlds.size()) + if (row < 0 || row >= this->m_worlds.size()) continue; - worlds_.append(this->worlds[row]); + worlds_.append(this->m_worlds[row]); } if (!worlds_.size()) { return new QMimeData(); @@ -397,7 +391,7 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; // files dropped from outside? if (data->hasUrls()) { - bool was_watching = is_watching; + bool was_watching = m_isWatching; if (was_watching) stopWatching(); auto urls = data->urls(); @@ -420,4 +414,44 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; } +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") { + return file.size(); + } else if (file.isDir()) { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void WorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) { + auto file = m_worlds.at(i).container(); + int row = i; + QThreadPool::globalInstance()->start([this, file, row]() mutable { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() { + if (row < m_worlds.size() && m_worlds[row].container() == file) { + m_worlds[row].setSize(size); + + // Notify views + QModelIndex modelIndex = index(row); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} + #include "WorldList.moc" diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index bea24bb9a..93fecf1f5 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -40,9 +40,9 @@ class WorldList : public QAbstractListModel { virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; - size_t size() const { return worlds.size(); }; + size_t size() const { return m_worlds.size(); }; bool empty() const { return size() == 0; } - World& operator[](size_t index) { return worlds[index]; } + World& operator[](size_t index) { return m_worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); @@ -82,10 +82,11 @@ class WorldList : public QAbstractListModel { QString instDirPath() const; - const QList& allWorlds() const { return worlds; } + const QList& allWorlds() const { return m_worlds; } private slots: void directoryChanged(QString path); + void loadWorldsAsync(); signals: void changed(); @@ -93,7 +94,7 @@ class WorldList : public QAbstractListModel { protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; - bool is_watching; + bool m_isWatching; QDir m_dir; - QList worlds; + QList m_worlds; }; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index fd2082035..9dbe7ffc0 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include namespace { diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 1ada4e38a..df7d569da 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -36,8 +36,8 @@ #pragma once #include #include +#include #include -#include #include #include diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index d276d4c41..3cbbb2a74 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -260,6 +260,30 @@ int AccountList::count() const return m_accounts.count(); } +QString getAccountStatus(AccountState status) +{ + switch (status) { + case AccountState::Unchecked: + return QObject::tr("Unchecked", "Account status"); + case AccountState::Offline: + return QObject::tr("Offline", "Account status"); + case AccountState::Online: + return QObject::tr("Ready", "Account status"); + case AccountState::Working: + return QObject::tr("Working", "Account status"); + case AccountState::Errored: + return QObject::tr("Errored", "Account status"); + case AccountState::Expired: + return QObject::tr("Expired", "Account status"); + case AccountState::Disabled: + return QObject::tr("Disabled", "Account status"); + case AccountState::Gone: + return QObject::tr("Gone", "Account status"); + default: + return QObject::tr("Unknown", "Account status"); + } +} + QVariant AccountList::data(const QModelIndex& index, int role) const { if (!index.isValid()) @@ -273,13 +297,10 @@ QVariant AccountList::data(const QModelIndex& index, int role) const switch (role) { case Qt::DisplayRole: switch (index.column()) { - case ProfileNameColumn: { + case ProfileNameColumn: return account->profileName(); - } - case NameColumn: return account->accountDisplayString(); - case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { @@ -291,39 +312,8 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } return tr("Unknown", "Account type"); } - - case StatusColumn: { - switch (account->accountState()) { - case AccountState::Unchecked: { - return tr("Unchecked", "Account status"); - } - case AccountState::Offline: { - return tr("Offline", "Account status"); - } - case AccountState::Online: { - return tr("Ready", "Account status"); - } - case AccountState::Working: { - return tr("Working", "Account status"); - } - case AccountState::Errored: { - return tr("Errored", "Account status"); - } - case AccountState::Expired: { - return tr("Expired", "Account status"); - } - case AccountState::Disabled: { - return tr("Disabled", "Account status"); - } - case AccountState::Gone: { - return tr("Gone", "Account status"); - } - default: { - return tr("Unknown", "Account status"); - } - } - } - + case StatusColumn: + return getAccountStatus(account->accountState()); default: return QVariant(); } @@ -335,11 +325,9 @@ QVariant AccountList::data(const QModelIndex& index, int role) const return QVariant::fromValue(account); case Qt::CheckStateRole: - if (index.column() == ProfileNameColumn) { + if (index.column() == ProfileNameColumn) return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); - } + return QVariant(); default: return QVariant(); @@ -461,18 +449,14 @@ bool AccountList::loadList() // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); - switch (listVersion) { - case AccountListVersion::MojangMSA: { - return loadV3(root); - } break; - default: { - QString newName = "accounts-old.json"; - qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; - // Attempt to rename the old version. - file.rename(newName); - return false; - } - } + if (listVersion == AccountListVersion::MojangMSA) + return loadV3(root); + + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; } bool AccountList::loadV3(QJsonObject& root) diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 45926206c..287831b2f 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -19,7 +18,7 @@ #include -AuthFlow::AuthFlow(AccountData* data, Action action, QObject* parent) : Task(parent), m_data(data) +AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) { if (data->type == AccountType::MSA) { if (action == Action::DeviceCode) { diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index 4d18ac845..710509d8e 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -5,7 +5,6 @@ #include #include #include -#include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" @@ -17,7 +16,7 @@ class AuthFlow : public Task { public: enum class Action { Refresh, Login, DeviceCode }; - explicit AuthFlow(AccountData* data, Action action = Action::Refresh, QObject* parent = 0); + explicit AuthFlow(AccountData* data, Action action = Action::Refresh); virtual ~AuthFlow() = default; void executeTask() override; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 54e7d69e0..cbe604805 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -1,12 +1,9 @@ #pragma once -#include #include #include -#include "QObjectPtr.h" class MinecraftAccount; -class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 5b063604c..86e9cc511 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,7 +55,8 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + static const QRegularExpression s_removeChars("[{}-]"); + data.internalId = QUuid::createUuid().toString().remove(s_removeChars); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -76,14 +77,15 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { + static const QRegularExpression s_removeChars("[{}-]"); auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; 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.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(s_removeChars); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -106,11 +108,7 @@ QPixmap MinecraftAccount::getFace() const return QPixmap(); } 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)); @@ -121,7 +119,7 @@ shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) { Q_ASSERT(m_currentTask.get() == nullptr); - m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login, this)); + m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); @@ -135,7 +133,7 @@ shared_qobject_ptr MinecraftAccount::refresh() return m_currentTask; } - m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh, this)); + m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); @@ -235,6 +233,7 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { + static const QRegularExpression s_removeChars("[{}-]"); if (ownsMinecraft() && !hasProfile()) { session->status = AuthSession::RequiresProfileSetup; } else { @@ -252,7 +251,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]")); + session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { @@ -290,13 +289,8 @@ QUuid MinecraftAccount::uuidFromUsername(QString username) // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - auto bOr = [](QByteArray& array, int index, char value) { array[index] = array.at(index) | value; }; - auto bAnd = [](QByteArray& array, int index, char value) { array[index] = array.at(index) & value; }; -#else auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; -#endif bAnd(digest, 6, (char)0x0f); // clear version bOr(digest, 6, (char)0x30); // set to version 3 bAnd(digest, 8, (char)0x3f); // clear variant diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index f9d89baa2..de1ffda86 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -315,11 +315,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) auto value = pObj.value("value"); if (value.isString()) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); -#else - texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); -#endif } if (!texturePayload.isEmpty()) { diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index c283b153e..38ff90a47 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -74,12 +74,12 @@ void MSADeviceCodeStep::perform() m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAutorizationFinished); + connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); m_task->start(); } -struct DeviceAutorizationResponse { +struct DeviceAuthorizationResponse { QString device_code; QString user_code; QString verification_uri; @@ -90,17 +90,17 @@ struct DeviceAutorizationResponse { QString error_description; }; -DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& data) +DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device autorization response due to err:" << err.errorString(); + qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { - qWarning() << "Device autorization response is not an object"; + qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); @@ -111,9 +111,9 @@ DeviceAutorizationResponse parseDeviceAutorizationResponse(const QByteArray& dat }; } -void MSADeviceCodeStep::deviceAutorizationFinished() +void MSADeviceCodeStep::deviceAuthorizationFinished() { - auto rsp = parseDeviceAutorizationResponse(*m_response); + auto rsp = parseDeviceAuthorizationResponse(*m_response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, @@ -208,12 +208,12 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse device autorization response due to err:" << err.errorString(); + qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { - qWarning() << "Device autorization response is not an object"; + qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); @@ -274,4 +274,4 @@ void MSADeviceCodeStep::authenticationFinished() m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; emit finished(AccountTaskState::STATE_WORKING, tr("Got")); -} \ No newline at end of file +} diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index 616008def..7f755563f 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -58,7 +58,7 @@ class MSADeviceCodeStep : public AuthStep { void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: - void deviceAutorizationFinished(); + void deviceAuthorizationFinished(); void startPoolTimer(); void authenticateUser(); void authenticationFinished(); diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 74999414c..aa972be71 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -85,8 +85,7 @@ class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) { m_clientId = APPLICATION->getMSAClientID(); - if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || - QFile::exists(FS::PathCombine(APPLICATION->root(), "portable.txt")) || !isSchemeHandlerRegistered()) + if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { auto replyHandler = new QOAuthHttpServerReplyHandler(this); @@ -100,29 +99,29 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile )XXX") .arg(BuildConfig.LOGIN_CALLBACK_URL)); - oauth2.setReplyHandler(replyHandler); + m_oauth2.setReplyHandler(replyHandler); } else { - oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); + m_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()); + m_oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); + m_oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); + m_oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); + m_oauth2.setClientIdentifier(m_clientId); + m_oauth2.setNetworkAccessManager(APPLICATION->network().get()); - connect(&oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { - m_data->msaClientID = oauth2.clientIdentifier(); + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { + m_data->msaClientID = m_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(); + m_data->msaToken.notAfter = m_oauth2.expirationAt(); + m_data->msaToken.extra = m_oauth2.extraTokens(); + m_data->msaToken.refresh_token = m_oauth2.refreshToken(); + m_data->msaToken.token = m_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) { + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) { auto state = AccountTaskState::STATE_FAILED_HARD; - if (oauth2.status() == QAbstractOAuth::Status::Granted || silent) { + if (m_oauth2.status() == QAbstractOAuth::Status::Granted || silent) { if (err == QAbstractOAuth2::Error::NetworkError) { state = AccountTaskState::STATE_OFFLINE; } else { @@ -136,16 +135,16 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile qWarning() << message; emit finished(state, message); }); - connect(&oauth2, &QOAuth2AuthorizationCodeFlow::error, this, + connect(&m_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, + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); - connect(&oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this, + connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this, [this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; }); } @@ -166,21 +165,16 @@ void MSAStep::perform() emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty.")); return; } - oauth2.setRefreshToken(m_data->msaToken.refresh_token); - oauth2.refreshAccessToken(); + m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); + m_oauth2.refreshAccessToken(); } else { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 - oauth2.setModifyParametersFunction( + m_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; - oauth2.grant(); + m_oauth2.grant(); } } -#include "MSAStep.moc" \ No newline at end of file +#include "MSAStep.moc" diff --git a/launcher/minecraft/auth/steps/MSAStep.h b/launcher/minecraft/auth/steps/MSAStep.h index 675cfb2ca..2f4e7812b 100644 --- a/launcher/minecraft/auth/steps/MSAStep.h +++ b/launcher/minecraft/auth/steps/MSAStep.h @@ -55,5 +55,5 @@ class MSAStep : public AuthStep { private: bool m_silent; QString m_clientId; - QOAuth2AuthorizationCodeFlow oauth2; + QOAuth2AuthorizationCodeFlow m_oauth2; }; diff --git a/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp index 4f4fb99a7..25f7074ec 100644 --- a/launcher/minecraft/gameoptions/GameOptions.cpp +++ b/launcher/minecraft/gameoptions/GameOptions.cpp @@ -87,16 +87,12 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const if (row < 0 || row >= int(contents.size())) return QVariant(); - switch (role) { - case Qt::DisplayRole: - if (column == 0) { - return contents[row].key; - } else { - return contents[row].value; - } - default: - return QVariant(); + if (role == Qt::DisplayRole) { + if (column == 0) + return contents[row].key; + return contents[row].value; } + return QVariant(); } int GameOptions::rowCount(const QModelIndex&) const diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index b23b23ed2..854590dd2 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -57,9 +57,7 @@ #include "tasks/SequentialTask.h" AutoInstallJava::AutoInstallJava(LaunchTask* parent) - : LaunchStep(parent) - , m_instance(m_parent->instance()) - , m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; + : LaunchStep(parent), m_instance(m_parent->instance()), m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; void AutoInstallJava::executeTask() { @@ -78,7 +76,7 @@ void AutoInstallJava::executeTask() auto java = std::dynamic_pointer_cast(javas->at(i)); if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->id.major())) { if (!java->is_64bit) { - emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Info); + emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Launcher); } setJavaPath(java->path); return; @@ -136,7 +134,7 @@ void AutoInstallJava::setJavaPath(QString path) settings->set("OverrideJavaLocation", true); settings->set("JavaPath", path); settings->set("AutomaticJava", true); - emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Info); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); emitSucceeded(); } @@ -179,7 +177,7 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) return; } #if defined(Q_OS_MACOS) - auto seq = makeShared(this, tr("Install Java")); + auto seq = makeShared(tr("Install Java")); seq->addTask(m_current_task); seq->addTask(makeShared(final_path)); m_current_task = seq; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d9a2b9b6b..388d55628 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -53,15 +53,16 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) { if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { + static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = - connect(&m_process, &LoggedProcess::log, this, [=](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { - qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { - APPLICATION->closeAllWindows(); - disconnect(*connection); - } - }); + *connection = connect(&m_process, &LoggedProcess::log, this, + [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(s_settingUser).length() != 0) { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); @@ -131,6 +132,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); @@ -170,6 +172,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; } diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 4a9e77a70..580d5c714 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -105,19 +105,16 @@ std::pair DataPack::compatibleVersions() const int DataPack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: - return Resource::compare(other, type); - case SortType::PACK_FORMAT: { - auto this_ver = packFormat(); - auto other_ver = cast_other.packFormat(); + if (type == SortType::PACK_FORMAT) { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); - if (this_ver > other_ver) - return 1; - if (this_ver < other_ver) - return -1; - break; - } + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + } else { + return Resource::compare(other, type); } return 0; } diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 4855b0203..266c2061b 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -35,8 +35,6 @@ class Version; class DataPack : public Resource { Q_OBJECT public: - using Ptr = shared_qobject_ptr; - DataPack(QObject* parent = nullptr) : Resource(parent) {} DataPack(QFileInfo file_info) : Resource(file_info) {} @@ -59,6 +57,8 @@ class DataPack : public Resource { [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + virtual QString directory() { return "/data"; } + protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index fb3a10133..0b8cb124d 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -26,33 +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); +} - static auto modSideToString(ModSide side) -> QString { return Packwiz::V1::sideToString(side); } -}; +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 9663026cd..99fc39ce0 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -38,16 +38,15 @@ #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/Resource.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" @@ -56,24 +55,6 @@ 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; @@ -101,33 +82,28 @@ int Mod::compare(const Resource& other, SortType type) const return -1; break; } - case SortType::PROVIDER: { - return QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); - } case SortType::SIDE: { - if (side() > cast_other->side()) - return 1; - else if (side() < cast_other->side()) - return -1; - break; - } - case SortType::LOADERS: { - if (loaders() > cast_other->loaders()) - return 1; - else if (loaders() < cast_other->loaders()) - return -1; + auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } case SortType::MC_VERSIONS: { - auto thisVersion = mcVersions().join(","); - auto otherVersion = cast_other->mcVersions().join(","); - return QString::compare(thisVersion, otherVersion, Qt::CaseInsensitive); + 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: { - if (releaseType() > cast_other->releaseType()) - return 1; - else if (releaseType() < cast_other->releaseType()) - return -1; + auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } } @@ -148,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; @@ -181,10 +135,16 @@ auto Mod::name() const -> QString if (!d_name.isEmpty()) return d_name; - if (metadata()) - return metadata()->name; + return Resource::name(); +} - return m_name; +auto Mod::mod_id() const -> QString +{ + auto d_mod_id = details().mod_id; + if (!d_mod_id.isEmpty()) + return d_mod_id; + + return Resource::name(); } auto Mod::version() const -> QString @@ -192,16 +152,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 @@ -214,73 +210,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_packImageCacheKey.wasReadAttempt = false; } } -auto Mod::provider() const -> std::optional -{ - if (metadata()) - return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); - return {}; -} - -auto Mod::side() const -> Metadata::ModSide -{ - if (metadata()) - return metadata()->side; - return Metadata::ModSide::UniversalSide; -} - -auto Mod::releaseType() const -> ModPlatform::IndexedVersionType -{ - if (metadata()) - return metadata()->releaseType; - return ModPlatform::IndexedVersionType::VersionType::Unknown; -} - -auto Mod::loaders() const -> ModPlatform::ModLoaderTypes -{ - if (metadata()) - return metadata()->loaders; - return {}; -} - -auto Mod::mcVersions() const -> QStringList -{ - if (metadata()) - return metadata()->mcVersions; - return {}; -} - auto Mod::licenses() const -> const QList& { return details().licenses; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index a0d9797ed..553af92f3 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -48,7 +48,6 @@ #include "ModDetails.h" #include "Resource.h" -#include "modplatform/ModIndex.h" class Mod : public Resource { Q_OBJECT @@ -58,24 +57,21 @@ 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 mod_id() const -> QString; 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 -> Metadata::ModSide; - auto loaders() const -> ModPlatform::ModLoaderTypes; - auto mcVersions() const -> QStringList; - auto releaseType() const -> ModPlatform::IndexedVersionType; + 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; } @@ -84,17 +80,11 @@ class Mod : public Resource { /** Thread-safe. */ 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]] int compare(Resource const& other, SortType type) const 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 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 5e4fe7f11..43888ae27 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -51,18 +51,10 @@ #include "Application.h" -#include "Json.h" -#include "minecraft/mod/MetadataHandler.h" -#include "minecraft/mod/Resource.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", "Size", "Side", "Loaders", "Minecraft Versions", "Release Type" }); @@ -92,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: @@ -100,64 +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 Metadata::modSideToString(at(row)->side()); + return at(row).side(); } case LoadersColumn: { - QStringList loaders; - auto modLoaders = at(row)->loaders(); - for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, - ModPlatform::Fabric, ModPlatform::Quilt }) { - if (modLoaders & loader) { - loaders << getModLoaderAsString(loader); - } - } - return loaders.join(", "); + return at(row).loaders(); } case McVersionsColumn: { - return at(row)->mcVersions().join(", "); + return at(row).mcVersions(); } case ReleaseTypeColumn: { - return at(row)->releaseType().toString(); + return at(row).releaseType(); } case SizeColumn: - return m_resources[row]->sizeStr(); + return at(row).sizeStr(); default: return QVariant(); } 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."); } } 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)->icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -167,12 +145,9 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const } return {}; case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); default: return QVariant(); } @@ -210,7 +185,7 @@ 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: @@ -235,133 +210,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->getOriginalFileName() == 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); @@ -379,51 +237,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 db5edf66e..42868dc91 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -47,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; @@ -76,8 +71,7 @@ class ModFolderModel : public ResourceFolderModel { ReleaseTypeColumn, NUM_COLUMNS }; - enum ModStatusAction { Disable, Enable, Toggle }; - ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); + ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "mods"; } @@ -86,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 c52d76f1f..6f2efe740 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -21,7 +21,7 @@ void Resource::setFile(QFileInfo file_info) parseFile(); } -std::tuple calculateFileSize(const QFileInfo& file) +static std::tuple calculateFileSize(const QFileInfo& file) { if (file.isDir()) { auto dir = QDir(file.absoluteFilePath()); @@ -72,13 +72,45 @@ 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); - string.remove(regex); + static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); + string.remove(s_regex); string = string.trimmed(); } +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) { @@ -93,6 +125,7 @@ int Resource::compare(const Resource& other, SortType type) const 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); @@ -118,6 +151,12 @@ int Resource::compare(const Resource& other, SortType type) const 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; @@ -174,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 diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index c99dab8db..16d8c2a89 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -40,6 +40,7 @@ #include #include +#include "MetadataHandler.h" #include "QObjectPtr.h" enum class ResourceType { @@ -50,7 +51,14 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, LOADERS, MC_VERSIONS, RELEASE_TYPE }; +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 }; @@ -84,9 +92,19 @@ class Resource : public QObject { [[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' @@ -117,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(); } @@ -132,6 +152,9 @@ class Resource : public QObject { [[nodiscard]] bool isMoreThanOneHardLink() const; + [[nodiscard]] auto mod_id() const -> QString { return m_mod_id; } + void setModId(const QString& modId) { m_mod_id = modId; } + protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; @@ -142,10 +165,16 @@ class Resource : public QObject { QString m_internal_id; /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ QString m_name; + QString m_mod_id; /* 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; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index ce3e16bce..bf40c81d7 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -11,20 +11,23 @@ #include #include #include +#include #include "Application.h" #include "FileSystem.h" -#include "QVariantUtils.h" -#include "StringUtils.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()); @@ -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; @@ -158,11 +164,51 @@ bool ResourceFolderModel::installResource(QString original_path) return false; } -bool ResourceFolderModel::uninstallResource(QString file_name) +void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +{ + auto install = [this, path] { installResource(std::move(path)); }; + if (vers.addonId.isValid()) { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + auto response = std::make_shared(); + auto job = FlameAPI().getProject(vers.addonId.toString(), response); + QObject::connect(job.get(), &Task::failed, this, install); + QObject::connect(job.get(), &Task::aborted, this, install); + QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, install, &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, this, install); + update_metadata.start(); + }); + + job->start(); + } else { + install(); + } +} + +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 +224,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 +236,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()) @@ -245,7 +305,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; @@ -261,7 +321,7 @@ bool ResourceFolderModel::update() return true; } -void ResourceFolderModel::resolveResource(Resource* res) +void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; @@ -277,11 +337,14 @@ void ResourceFolderModel::resolveResource(Resource* res) m_active_parse_tasks.insert(ticket, task); connect( - task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); - connect(task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::finished, this, - [=] { + [this, ticket] { m_active_parse_tasks.remove(ticket); emit parseFinished(); }, @@ -296,20 +359,15 @@ 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; -#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_resources.keys(); QSet new_set(new_list.begin(), new_list.end()); -#else - QSet current_set(m_resources_index.keys().toSet()); - QSet new_set(new_resources.keys().toSet()); -#endif applyUpdates(current_set, new_set, new_resources); } @@ -317,7 +375,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]; @@ -326,7 +384,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 @@ -416,6 +478,8 @@ QVariant ResourceFolderModel::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: @@ -444,12 +508,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return {}; } case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return {}; - } + if (column == ActiveColumn) + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return {}; default: return {}; } @@ -487,22 +548,23 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien 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 ActiveColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("Is the resource enabled?"); case NameColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The name of the resource."); case DateColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The date and time this resource was last changed (or added)."); + case ProviderColumn: + return tr("The source provider of the resource."); case SizeColumn: return tr("The size of the resource."); default: @@ -526,8 +588,7 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { 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); + auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); setting->set(tree->header()->saveState()); } @@ -539,8 +600,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) } 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); + auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); tree->header()->restoreState(setting->get().toByteArray()); } @@ -629,7 +689,7 @@ QString ResourceFolderModel::instDirPath() const void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd()) + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; auto removed_index = m_resources_index[resource_id]; @@ -648,3 +708,126 @@ void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) } 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 ca919d3e3..f6173b0d9 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 void 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 { ActiveColumn = 0, NameColumn, DateColumn, SizeColumn, 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()); } @@ -153,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(). * @@ -167,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); @@ -195,19 +237,22 @@ class ResourceFolderModel : public QAbstractListModel { protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE }; - QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Size") }; + QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, true, true }; - QList m_columnsHiddenByDefault = { false, false, false, false }; + 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; @@ -220,133 +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()); - - 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 81fb91485..cf8ec871c 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -3,8 +3,6 @@ #include #include #include -#include - #include "MTPixmapCache.h" #include "Version.h" @@ -42,13 +40,6 @@ void ResourcePack::setPackFormat(int new_format_id) m_pack_format = new_format_id; } -void ResourcePack::setDescription(QString new_description) -{ - QMutexLocker locker(&m_data_lock); - - m_description = new_description; -} - void ResourcePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); @@ -90,7 +81,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const } // Imaged got evicted from the cache. Re-process it and retry. - ResourcePackUtils::processPackPNG(*this); + ResourcePackUtils::processPackPNG(this); return image(size); } @@ -102,44 +93,3 @@ std::pair ResourcePack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } - -int ResourcePack::compare(const Resource& other, SortType type) const -{ - auto const& cast_other = static_cast(other); - switch (type) { - 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; - if (this_ver < other_ver) - return -1; - break; - } - } - return 0; -} - -bool ResourcePack::applyFilter(QRegularExpression filter) const -{ - if (filter.match(description()).hasMatch()) - return true; - - if (filter.match(QString::number(packFormat())).hasMatch()) - return true; - - if (filter.match(compatibleVersions().first.toString()).hasMatch()) - return true; - if (filter.match(compatibleVersions().second.toString()).hasMatch()) - return true; - - return Resource::applyFilter(filter); -} - -bool ResourcePack::valid() const -{ - return m_pack_format != 0; -} diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 2254fc5c4..bb5aeecb5 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -1,6 +1,7 @@ #pragma once #include "Resource.h" +#include "minecraft/mod/DataPack.h" #include #include @@ -14,51 +15,27 @@ class Version; * Store localized descriptions * */ -class ResourcePack : public Resource { +class ResourcePack : public DataPack { Q_OBJECT public: - using Ptr = shared_qobject_ptr; + ResourcePack(QObject* parent = nullptr) : DataPack(parent) {} + ResourcePack(QFileInfo file_info) : DataPack(file_info) {} - ResourcePack(QObject* parent = nullptr) : Resource(parent) {} - ResourcePack(QFileInfo file_info) : Resource(file_info) {} - - /** Gets the numerical ID of the pack format. */ - [[nodiscard]] int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ [[nodiscard]] std::pair compatibleVersions() const; - /** Gets the description of the resource pack. */ - [[nodiscard]] QString description() const { return m_description; } - /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; - /** Thread-safe. */ - void setPackFormat(int new_format_id); - - /** Thread-safe. */ - void setDescription(QString new_description); - /** Thread-safe. */ void setImage(QImage new_image) const; - bool valid() const override; + /** Thread-safe. */ + void setPackFormat(int new_format_id); - [[nodiscard]] int compare(Resource const& other, SortType type) const override; - [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + virtual QString directory() { return "/assets"; } protected: - mutable QMutex m_data_lock; - - /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta - */ - int m_pack_format = 0; - - /** The resource pack's description, as defined in the pack.mcmeta file. - */ - QString m_description; - /** The resource pack's image file cache key, for access in the QPixmapCache global instance. * * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 1aa3a2e8c..d9f27a043 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -44,19 +44,19 @@ #include "Application.h" #include "Version.h" -#include "minecraft/mod/Resource.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.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", "Size" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + 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 }; - m_columnsHiddenByDefault = { false, false, false, false, false, false }; + m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -73,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); @@ -87,17 +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 {}; } @@ -107,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."); } @@ -127,12 +128,9 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } return {}; case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return {}; - } + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + return {}; default: return {}; } @@ -148,6 +146,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case PackFormatColumn: case DateColumn: case ImageColumn: + case ProviderColumn: case SizeColumn: return columnNames().at(section); default: @@ -157,7 +156,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: @@ -165,6 +164,8 @@ 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: @@ -185,12 +186,7 @@ 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)); + return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 755b9c4c6..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, SizeColumn, 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 ccb344cb5..99e51fcae 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -22,8 +22,6 @@ #include "ShaderPack.h" -#include - void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&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/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 04cc36310..a1ef7f525 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -21,8 +21,6 @@ #include #include -#include - #include "MTPixmapCache.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 48e940e9b..4d7c71359 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -39,22 +39,19 @@ #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", "Size" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; -} - -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) @@ -77,6 +74,8 @@ 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: @@ -84,14 +83,14 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const } 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."); } @@ -99,10 +98,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 {}; } @@ -130,6 +129,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case NameColumn: case DateColumn: case ImageColumn: + case ProviderColumn: case SizeColumn: return columnNames().at(section); default: @@ -138,14 +138,13 @@ 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 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 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 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: diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index de90f879f..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, SizeColumn, 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 fb2e22de6..000000000 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "Application.h" -#include "FileSystem.h" -#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 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); - } - 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 b9288d2b3..b63d36361 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -52,11 +52,10 @@ static bool checkDependencies(std::shared_ptrversion.loaders || sel->version.loaders & loaders); } -GetModDependenciesTask::GetModDependenciesTask(QObject* parent, - BaseInstance* instance, +GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected) - : SequentialTask(parent, tr("Get dependencies")) + : SequentialTask(tr("Get dependencies")) , m_selected(selected) , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), std::make_shared() } @@ -185,7 +184,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; auto tasks = makeShared( - this, QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); if (!dep.addonId.toString().isEmpty()) { tasks->addTask(getProjectInfoTask(pDep)); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 7202b01e0..a02ffb4d5 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -19,7 +19,6 @@ #pragma once #include -#include #include #include #include @@ -61,10 +60,7 @@ class GetModDependenciesTask : public SequentialTask { std::shared_ptr api; }; - explicit GetModDependenciesTask(QObject* parent, - BaseInstance* instance, - ModFolderModel* folder, - QList> selected); + explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 82f6b9df9..530ce44cc 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -23,6 +23,8 @@ #include "FileSystem.h" #include "Json.h" +#include "minecraft/mod/ResourcePack.h" +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include #include @@ -32,9 +34,9 @@ namespace DataPackUtils { -bool process(DataPack& pack, ProcessingLevel level) +bool process(DataPack* pack, ProcessingLevel level) { - switch (pack.type()) { + switch (pack->type()) { case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: @@ -45,16 +47,16 @@ bool process(DataPack& pack, ProcessingLevel level) } } -bool processFolder(DataPack& pack, ProcessingLevel level) +bool processFolder(DataPack* pack, ProcessingLevel level) { - Q_ASSERT(pack.type() == ResourceType::FOLDER); + Q_ASSERT(pack->type() == ResourceType::FOLDER); auto mcmeta_invalid = [&pack]() { - qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; - QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) @@ -72,7 +74,7 @@ bool processFolder(DataPack& pack, ProcessingLevel level) return mcmeta_invalid(); // mcmeta file isn't a valid file } - QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); + QFileInfo data_dir_info(FS::PathCombine(pack->fileinfo().filePath(), pack->directory())); if (!data_dir_info.exists() || !data_dir_info.isDir()) { return false; // data dir does not exists or isn't valid } @@ -80,22 +82,46 @@ bool processFolder(DataPack& pack, ProcessingLevel level) if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } + if (auto rp = dynamic_cast(pack)) { + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + } return true; // all tests passed } -bool processZIP(DataPack& pack, ProcessingLevel level) +bool processZIP(DataPack* pack, ProcessingLevel level) { - Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + Q_ASSERT(pack->type() == ResourceType::ZIPFILE); - QuaZip zip(pack.fileinfo().filePath()); + QuaZip zip(pack->fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) return false; // can't open zip file QuaZipFile file(&zip); auto mcmeta_invalid = [&pack]() { - qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; @@ -119,7 +145,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level) } QuaZipDir zipDir(&zip); - if (!zipDir.exists("/data")) { + if (!zipDir.exists(pack->directory())) { return false; // data dir does not exists at zip root } @@ -127,21 +153,49 @@ bool processZIP(DataPack& pack, ProcessingLevel level) zip.close(); return true; // only need basic info already checked } + if (auto rp = dynamic_cast(pack)) { + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data)); + + file.close(); + zip.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + } zip.close(); return true; } // https://minecraft.wiki/w/Data_pack#pack.mcmeta -bool processMCMeta(DataPack& pack, QByteArray&& raw_data) +// https://minecraft.wiki/w/Raw_JSON_text_format +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +bool processMCMeta(DataPack* pack, QByteArray&& raw_data) { try { auto json_doc = QJsonDocument::fromJson(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->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack->setDescription(ResourcePackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); return false; @@ -152,26 +206,19 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data) bool validate(QFileInfo file) { DataPack dp{ file }; - return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); + return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); } } // namespace DataPackUtils -LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {} - -bool LocalDataPackParseTask::abort() -{ - m_aborted = true; - return true; -} +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {} void LocalDataPackParseTask::executeTask() { - if (!DataPackUtils::process(m_data_pack)) + if (!DataPackUtils::process(m_data_pack)) { + emitFailed("process failed"); return; + } - if (m_aborted) - emitAborted(); - else - emitSucceeded(); + emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 12fd8c82c..7355783df 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -32,12 +32,12 @@ namespace DataPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; -bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); -bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); -bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); -bool processMCMeta(DataPack& pack, QByteArray&& raw_data); +bool processMCMeta(DataPack* pack, QByteArray&& raw_data); /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); @@ -47,10 +47,7 @@ bool validate(QFileInfo file); class LocalDataPackParseTask : public Task { Q_OBJECT public: - LocalDataPackParseTask(int token, DataPack& dp); - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override; + LocalDataPackParseTask(int token, DataPack* dp); void executeTask() override; @@ -59,7 +56,5 @@ class LocalDataPackParseTask : public Task { private: int m_token; - DataPack& m_data_pack; - - bool m_aborted = false; + DataPack* m_data_pack; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index d456211f8..952115bed 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 const QRegularExpression s_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 {}; } @@ -290,86 +293,90 @@ ModDetails ReadFabricModInfo(QByteArray contents) // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md ModDetails ReadQuiltModInfo(QByteArray contents) { - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); - auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); - ModDetails details; + try { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); + auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); - // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md - if (schemaVersion == 1) { - auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); + // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md + if (schemaVersion == 1) { + auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); - details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); - details.version = Json::requireString(modInfo.value("version"), "Mod version"); + details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details.version = Json::requireString(modInfo.value("version"), "Mod version"); - auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + auto modMetadata = Json::ensureObject(modInfo.value("metadata")); - details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); - details.description = Json::ensureString(modMetadata.value("description")); + details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); + details.description = Json::ensureString(modMetadata.value("description")); - auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + auto modContributors = Json::ensureObject(modMetadata.value("contributors")); - // We don't really care about the role of a contributor here - details.authors += modContributors.keys(); + // We don't really care about the role of a contributor here + details.authors += modContributors.keys(); - auto modContact = Json::ensureObject(modMetadata.value("contact")); + auto modContact = Json::ensureObject(modMetadata.value("contact")); - if (modContact.contains("homepage")) { - details.homeurl = Json::requireString(modContact.value("homepage")); - } - if (modContact.contains("issues")) { - details.issue_tracker = Json::requireString(modContact.value("issues")); - } + if (modContact.contains("homepage")) { + details.homeurl = Json::requireString(modContact.value("homepage")); + } + if (modContact.contains("issues")) { + details.issue_tracker = Json::requireString(modContact.value("issues")); + } - if (modMetadata.contains("license")) { - auto license = modMetadata.value("license"); - if (license.isArray()) { - for (auto l : license.toArray()) { - if (l.isString()) { - details.licenses.append(ModLicense(l.toString())); - } else if (l.isObject()) { - auto obj = l.toObject(); - details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), - obj.value("url").toString(), obj.value("description").toString())); + if (modMetadata.contains("license")) { + auto license = modMetadata.value("license"); + if (license.isArray()) { + for (auto l : license.toArray()) { + if (l.isString()) { + details.licenses.append(ModLicense(l.toString())); + } else if (l.isObject()) { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } } + } else if (license.isString()) { + details.licenses.append(ModLicense(license.toString())); + } else if (license.isObject()) { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), + obj.value("url").toString(), obj.value("description").toString())); + } + } + + if (modMetadata.contains("icon")) { + auto icon = modMetadata.value("icon"); + if (icon.isObject()) { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) { + auto size = key.split('x').first().toInt(); + if (size > largest) { + largest = size; + } + } + if (largest > 0) { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } else { // parsing the sizes failed + // take the first + for (auto i : obj) { + details.icon_file = i.toString(); + break; + } + } + } else if (icon.isString()) { + details.icon_file = icon.toString(); } - } else if (license.isString()) { - details.licenses.append(ModLicense(license.toString())); - } else if (license.isObject()) { - auto obj = license.toObject(); - details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), - obj.value("description").toString())); } } - if (modMetadata.contains("icon")) { - auto icon = modMetadata.value("icon"); - if (icon.isObject()) { - auto obj = icon.toObject(); - // take the largest icon - int largest = 0; - for (auto key : obj.keys()) { - auto size = key.split('x').first().toInt(); - if (size > largest) { - largest = size; - } - } - if (largest > 0) { - auto key = QString::number(largest) + "x" + QString::number(largest); - details.icon_file = obj.value(key).toString(); - } else { // parsing the sizes failed - // take the first - for (auto i : obj) { - details.icon_file = i.toString(); - break; - } - } - } else if (icon.isString()) { - details.icon_file = icon.toString(); - } - } + } catch (const Exception& e) { + qWarning() << "Unable to parse mod info:" << e.cause(); } return details; } @@ -487,11 +494,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(s_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; } } @@ -730,7 +737,7 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap) } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) - : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) + : Task(false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} bool LocalModParseTask::abort() diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 91ee6f253..7ce5a84d2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -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 27fbf3c6d..db4b2e55c 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -20,6 +20,7 @@ #include "FileSystem.h" #include "Json.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include #include @@ -29,155 +30,6 @@ namespace ResourcePackUtils { -bool process(ResourcePack& pack, ProcessingLevel level) -{ - switch (pack.type()) { - case ResourceType::FOLDER: - return ResourcePackUtils::processFolder(pack, level); - case ResourceType::ZIPFILE: - return ResourcePackUtils::processZIP(pack, level); - default: - qWarning() << "Invalid type for resource pack parse task!"; - return false; - } -} - -bool processFolder(ResourcePack& pack, ProcessingLevel level) -{ - Q_ASSERT(pack.type() == ResourceType::FOLDER); - - auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; - return false; // the mcmeta is not optional - }; - - QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); - if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { - QFile mcmeta_file(mcmeta_file_info.filePath()); - if (!mcmeta_file.open(QIODevice::ReadOnly)) - return mcmeta_invalid(); // can't open mcmeta file - - auto data = mcmeta_file.readAll(); - - bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); - - mcmeta_file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid - } - } else { - return mcmeta_invalid(); // mcmeta file isn't a valid file - } - - QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets")); - if (!assets_dir_info.exists() || !assets_dir_info.isDir()) { - return false; // assets dir does not exists or isn't valid - } - - if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked - } - - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; - - QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file - - auto data = pack_png_file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. - } - - return true; // all tests passed -} - -bool processZIP(ResourcePack& pack, ProcessingLevel level) -{ - Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - - auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; - return false; // the mcmeta is not optional - }; - - if (zip.setCurrentFile("pack.mcmeta")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return mcmeta_invalid(); - } - - auto data = file.readAll(); - - bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); - - file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid - } - } else { - return mcmeta_invalid(); // could not set pack.mcmeta as current file. - } - - QuaZipDir zipDir(&zip); - if (!zipDir.exists("/assets")) { - return false; // assets dir does not exists at zip root - } - - if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); - return true; // only need basic info already checked - } - - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; - - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - zip.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. - } - - zip.close(); - return true; -} - QString buildStyle(const QJsonObject& obj) { QStringList styles; @@ -259,30 +111,11 @@ QString processComponent(const QJsonValue& value, bool strikethrough, bool under 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) -{ - try { - auto json_doc = QJsonDocument::fromJson(raw_data); - auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); - - pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - - pack.setDescription(processComponent(pack_obj.value("description"))); - - } catch (Json::JsonException& e) { - qWarning() << "JsonException: " << e.what() << e.cause(); - return false; - } - return true; -} - -bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data) +bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { - pack.setImage(img); + pack->setImage(img); } else { qWarning() << "Failed to parse pack.png."; return false; @@ -290,16 +123,16 @@ bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data) return true; } -bool processPackPNG(const ResourcePack& pack) +bool processPackPNG(const ResourcePack* pack) { auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return false; }; - switch (pack.type()) { + switch (pack->type()) { case ResourceType::FOLDER: { - QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) @@ -319,7 +152,7 @@ bool processPackPNG(const ResourcePack& pack) return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } case ResourceType::ZIPFILE: { - QuaZip zip(pack.fileinfo().filePath()); + QuaZip zip(pack->fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) return false; // can't open zip file @@ -353,30 +186,7 @@ bool processPackPNG(const ResourcePack& pack) bool validate(QFileInfo file) { ResourcePack rp{ file }; - return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); + return DataPackUtils::process(&rp, DataPackUtils::ProcessingLevel::BasicInfoOnly) && rp.valid(); } } // namespace ResourcePackUtils - -LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) - : Task(nullptr, false), m_token(token), m_resource_pack(rp) -{} - -bool LocalResourcePackParseTask::abort() -{ - m_aborted = true; - return true; -} - -void LocalResourcePackParseTask::executeTask() -{ - if (!ResourcePackUtils::process(m_resource_pack)) { - emitFailed("this is not a resource pack"); - return; - } - - if (m_aborted) - emitAborted(); - else - emitSucceeded(); -} diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index 97bf7b2ba..6b4378aa6 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -23,44 +23,14 @@ #include "minecraft/mod/ResourcePack.h" -#include "tasks/Task.h" - namespace ResourcePackUtils { -enum class ProcessingLevel { Full, BasicInfoOnly }; - -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); +bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data); /// processes ONLY the pack.png (rest of the pack may be invalid) -bool processPackPNG(const ResourcePack& pack); +bool processPackPNG(const ResourcePack* pack); /** Checks whether a file is valid as a resource pack or not. */ bool validate(QFileInfo file); } // namespace ResourcePackUtils - -class LocalResourcePackParseTask : public Task { - Q_OBJECT - public: - LocalResourcePackParseTask(int token, ResourcePack& rp); - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override; - - void executeTask() override; - - [[nodiscard]] int token() const { return m_token; } - - private: - int m_token; - - ResourcePack& m_resource_pack; - - bool m_aborted = false; -}; 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 5447083ba..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 4deebcd1d..a6ecc5353 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -93,7 +93,7 @@ bool validate(QFileInfo file) } // namespace ShaderPackUtils -LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {} +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 00cc2def2..18020808a 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -230,8 +230,7 @@ bool validate(QFileInfo file) } // namespace TexturePackUtils -LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(nullptr, false), m_token(token), m_texture_pack(rp) -{} +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(false), m_token(token), m_texture_pack(rp) {} bool LocalTexturePackParseTask::abort() { diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index 9d564ddb3..d45f537fa 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() { @@ -180,8 +180,10 @@ bool LocalWorldSaveParseTask::abort() void LocalWorldSaveParseTask::executeTask() { - if (!WorldSaveUtils::process(m_save)) + if (!WorldSaveUtils::process(m_save)) { + emitFailed("this is not a world"); return; + } if (m_aborted) emitAborted(); diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp similarity index 57% rename from launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp rename to launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 63996e584..98dab9abb 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -34,7 +34,7 @@ * limitations under the License. */ -#include "ModFolderLoadTask.h" +#include "ResourceFolderLoadTask.h" #include "Application.h" #include "FileSystem.h" @@ -42,17 +42,22 @@ #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); @@ -63,8 +68,8 @@ void ModFolderLoadTask::executeTask() } // Read JAR files that don't have metadata - m_mods_dir.refresh(); - for (auto entry : m_mods_dir.entryInfoList()) { + 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; @@ -74,32 +79,33 @@ void ModFolderLoadTask::executeTask() FS::move(filePath, newFilePath); entry = QFileInfo(newFilePath); } - Mod* mod(new Mod(entry)); - 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); } } } @@ -107,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) @@ -126,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/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 017cb8dc2..56379aaab 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -31,7 +31,7 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA 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; + m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); directoryChanged(path); @@ -39,12 +39,12 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA void SkinList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -54,11 +54,11 @@ void SkinList::startWatching() void SkinList::stopWatching() { save(); - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -67,7 +67,7 @@ void SkinList::stopWatching() bool SkinList::update() { - QVector newSkins; + QList newSkins; m_dir.refresh(); auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); @@ -142,7 +142,7 @@ bool SkinList::update() 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); + m_skinList.swap(newSkins); endResetModel(); if (needsSave) save(); @@ -158,7 +158,7 @@ void SkinList::directoryChanged(const QString& path) if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); - if (is_watching) + if (m_isWatching) stopWatching(); startWatching(); } @@ -172,9 +172,9 @@ void SkinList::fileChanged(const QString& 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(); + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) { + m_skinList[i].refresh(); dataChanged(index(i), index(i)); break; } @@ -235,12 +235,17 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int row = index.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return QVariant(); - auto skin = m_skin_list[row]; + auto skin = m_skinList[row]; switch (role) { - case Qt::DecorationRole: - return skin.getTexture(); + case Qt::DecorationRole: { + auto preview = skin.getPreview(); + if (preview.isNull()) { + preview = skin.getTexture(); + } + return preview; + } case Qt::DisplayRole: return skin.name(); case Qt::UserRole: @@ -254,7 +259,7 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int SkinList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : m_skin_list.size(); + return parent.isValid() ? 0 : m_skinList.size(); } void SkinList::installSkins(const QStringList& iconFiles) @@ -284,8 +289,8 @@ QString SkinList::installSkin(const QString& file, const QString& name) int SkinList::getSkinIndex(const QString& key) const { - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].name() == key) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].name() == key) { return i; } } @@ -297,7 +302,7 @@ const SkinModel* SkinList::skin(const QString& key) const int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } SkinModel* SkinList::skin(const QString& key) @@ -305,22 +310,22 @@ SkinModel* SkinList::skin(const QString& key) int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } -bool SkinList::deleteSkin(const QString& key, const bool trash) +bool SkinList::deleteSkin(const QString& key, bool trash) { int idx = getSkinIndex(key); if (idx != -1) { - auto s = m_skin_list[idx]; + auto s = m_skinList[idx]; if (trash) { if (FS::trash(s.getPath(), nullptr)) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } } else if (QFile::remove(s.getPath())) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } @@ -332,7 +337,7 @@ void SkinList::save() { QJsonObject doc; QJsonArray arr; - for (auto s : m_skin_list) { + for (auto s : m_skinList) { arr << s.toJSON(); } doc["skins"] = arr; @@ -346,8 +351,8 @@ void SkinList::save() 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) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getURL() == skin.url) { return i; } } @@ -361,9 +366,9 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) } int row = idx.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return false; - auto& skin = m_skin_list[row]; + auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { skin.rename(newName); @@ -375,18 +380,18 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) 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()); + for (auto i = 0; i < m_skinList.size(); i++) { + if (m_skinList[i].getPath() == s->getPath()) { + m_skinList[i].setCapeId(s->getCapeId()); + m_skinList[i].setModel(s->getModel()); + m_skinList[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); + beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); + m_skinList.append(*s); endInsertRows(); } save(); diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index 66af6a17b..5a160909a 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -43,7 +43,7 @@ class SkinList : public QAbstractListModel { 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); + bool deleteSkin(const QString& key, bool trash); void installSkins(const QStringList& iconFiles); QString installSkin(const QString& file, const QString& name = {}); @@ -73,8 +73,8 @@ class SkinList : public QAbstractListModel { private: shared_qobject_ptr m_watcher; - bool is_watching; - QVector m_skin_list; + bool m_isWatching; + QList m_skinList; 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 index 937864e2c..b609bc6c7 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -18,17 +18,91 @@ #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) {} +static QImage improveSkin(const QImage& skin) +{ + if (skin.size() == QSize(64, 32)) { // old format + QImage newSkin = QImage(QSize(64, 64), skin.format()); + newSkin.fill(Qt::transparent); + QPainter p(&newSkin); + p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head + + auto leg = skin.copy(QRect(0, 16, 16, 16)); + p.drawImage(QPoint(16, 48), leg); // copy leg + + auto arm = skin.copy(QRect(40, 16, 16, 16)); + p.drawImage(QPoint(32, 48), arm); // copy arm + return newSkin; + } + return skin; +} +static QImage getSkin(const QString path) +{ + return improveSkin(QImage(path)); +} + +static QImage generatePreviews(QImage texture, bool slim) +{ + QImage preview(36, 36, QImage::Format_ARGB32); + preview.fill(Qt::transparent); + QPainter paint(&preview); + + // head + paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); + paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); + // torso + paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); + paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); + // right leg + paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); + paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); + // left leg + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + + auto armWidth = slim ? 3 : 4; + auto armPosX = slim ? 1 : 0; + // right arm + paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); + paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); + // left arm + paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); + paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); + + // back + // head + paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); + paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); + // torso + paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); + paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); + // right leg + paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); + paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); + // left leg + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + + // right arm + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); + // left arm + paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); + paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); + + return preview; +} +SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) +{ + m_preview = generatePreviews(m_texture, false); +} SkinModel::SkinModel(QDir skinDir, QJsonObject obj) - : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) + : m_capeId(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) { auto name = Json::ensureString(obj, "name"); @@ -36,7 +110,8 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; - m_texture = QPixmap(m_path); + m_texture = QImage(getSkin(m_path)); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } QString SkinModel::name() const @@ -55,7 +130,7 @@ QJsonObject SkinModel::toJSON() const { QJsonObject obj; obj["name"] = name(); - obj["capeId"] = m_cape_id; + obj["capeId"] = m_capeId; obj["url"] = m_url; obj["model"] = getModelString(); return obj; @@ -76,3 +151,13 @@ bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } +void SkinModel::refresh() +{ + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} +void SkinModel::setModel(Model model) +{ + m_model = model; + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h index 46e9d6cf1..711d7edb8 100644 --- a/launcher/minecraft/skins/SkinModel.h +++ b/launcher/minecraft/skins/SkinModel.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include class SkinModel { public: @@ -35,23 +35,25 @@ class SkinModel { 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; } + QImage getTexture() const { return m_texture; } + QImage getPreview() const { return m_preview; } + QString getCapeId() const { return m_capeId; } 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 setCapeId(QString capeID) { m_capeId = capeID; } + void setModel(Model model); void setURL(QString url) { m_url = url; } - void refresh() { m_texture = QPixmap(m_path); } + void refresh(); QJsonObject toJSON() const; private: QString m_path; - QPixmap m_texture; - QString m_cape_id; + QImage m_texture; + QImage m_preview; + QString m_capeId; Model m_model; QString m_url; }; \ No newline at end of file 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/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 24b82c28e..1ee820a63 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -3,6 +3,7 @@ #include "minecraft/mod/Mod.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" #include "tasks/Task.h" class ResourceDownloadTask; @@ -12,13 +13,18 @@ class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(QList& mods, + CheckUpdateTask(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder) - : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders_list(loadersList), m_mods_folder(mods_folder) {}; + 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; @@ -30,28 +36,28 @@ class CheckUpdateTask : public Task { 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, - bool enabled = true) - : 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: @@ -61,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; QList m_loaders_list; - std::shared_ptr m_mods_folder; + 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 f6f49f38d..e170fbcd0 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -6,8 +6,9 @@ #include "Application.h" #include "Json.h" +#include "QObjectPtr.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" @@ -18,55 +19,57 @@ 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_indexDir(dir), m_provider(prov), m_hashingTask(nullptr), m_currentTask(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_indexDir(dir), m_provider(prov), m_currentTask(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); } } -EnsureMetadataTask::EnsureMetadataTask(QHash& mods, QDir dir, ModPlatform::ResourceProvider prov) - : Task(nullptr), m_mods(mods), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) + +EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_resources(resources), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) {} -Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod) +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; } @@ -79,32 +82,32 @@ bool EnsureMetadataTask::abort() // Prevent sending signals to a dead object disconnect(this, 0, 0, 0); - if (m_current_task) - return m_current_task->abort(); + if (m_currentTask) + return m_currentTask->abort(); return true; } 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); } } @@ -120,9 +123,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(); }; @@ -144,71 +147,65 @@ 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(); + if (m_currentTask) + m_currentTask.reset(); }); connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); - m_current_task = project_task; + m_currentTask = project_task; project_task->start(); }); - connect(version_task.get(), &Task::finished, [=] { - version_task->deleteLater(); - if (m_current_task) - m_current_task.reset(); - }); - - if (m_mods.size() > 1) + if (m_resources.size() > 1) setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); - else if (!m_mods.empty()) + else if (!m_resources.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") - .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_mods.begin().value()->name())); + .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); - m_current_task = version_task; + m_currentTask = 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); } } @@ -219,7 +216,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() 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) @@ -239,20 +236,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)); + m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } } } catch (Json::JsonException& e) { @@ -267,7 +264,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; - for (auto const& data : m_temp_versions) + for (auto const& data : m_tempVersions) addonIds.insert(data.addonId.toString(), data.hash); auto response = std::make_shared(); @@ -324,24 +321,17 @@ 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); - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - qDebug() << entries; - - emitFail(mod); - } + updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } }); @@ -354,7 +344,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()); } @@ -394,15 +384,15 @@ 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)); + m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } } catch (Json::JsonException& e) { @@ -417,9 +407,9 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; - for (auto const& hash : m_mods.keys()) { - if (m_temp_versions.contains(hash)) { - auto data = m_temp_versions.find(hash).value(); + for (auto const& hash : m_resources.keys()) { + if (m_tempVersions.contains(hash)) { + auto data = m_tempVersions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) @@ -464,21 +454,21 @@ 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(); + ModPlatform::IndexedPack pack; 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); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } + updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } } catch (Json::JsonException& e) { qDebug() << e.cause(); @@ -489,74 +479,38 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() return proj_task; } -void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) -{ - // Prevent file name mismatch - ver.fileName = mod->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); - QEventLoop loop; - - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - - update_metadata.start(); - - if (!update_metadata.isFinished()) - loop.exec(); - } - - auto metadata = Metadata::get(tmp_index_dir, pack.slug); - if (!metadata.isValid()) { - qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); - return; - } - - mod->setMetadata(metadata); - - emitReady(mod); -} - -void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +void EnsureMetadataTask::updateMetadata(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); + auto task = makeShared(m_indexDir, pack, ver); - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; + connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); }); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - - update_metadata.start(); - - if (!update_metadata.isFinished()) - loop.exec(); - } - - auto metadata = Metadata::get(tmp_index_dir, pack.slug); - if (!metadata.isValid()) { - qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); - return; - } - - mod->setMetadata(metadata); - - emitReady(mod); + m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task; + task->start(); } catch (Json::JsonException& e) { qDebug() << e.cause(); - emitFail(mod); + emitFail(resource); } } + +void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource) +{ + QDir tmpIndexDir(m_indexDir); + auto metadata = Metadata::get(tmpIndexDir, pack.slug); + if (!metadata.isValid()) { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(resource); + return; + } + + resource->setMetadata(metadata); + + emitReady(resource); +} diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 631b32ae7..3d8a8ba53 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -1,26 +1,27 @@ #pragma once #include "ModIndex.h" +#include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" +#include "minecraft/mod/Resource.h" #include "tasks/ConcurrentTask.h" -#include - class Mod; +class QDir; class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); - EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); - EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(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; @@ -29,35 +30,36 @@ class EnsureMetadataTask : public Task { private: // FIXME: Move to their own namespace - auto modrinthVersionsTask() -> Task::Ptr; - auto modrinthProjectsTask() -> Task::Ptr; + Task::Ptr modrinthVersionsTask(); + Task::Ptr modrinthProjectsTask(); - auto flameVersionsTask() -> Task::Ptr; - auto flameProjectsTask() -> Task::Ptr; + Task::Ptr flameVersionsTask(); + Task::Ptr flameProjectsTask(); // 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; + Hashing::Hasher::Ptr createNewHash(Resource*); + QString getExistingHash(Resource*); private slots: - void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); - void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); + void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource); signals: - void metadataReady(Mod*); - void metadataFailed(Mod*); + void metadataReady(Resource*); + void metadataFailed(Resource*); private: - QHash m_mods; - QDir m_index_dir; + QHash m_resources; + QDir m_indexDir; ModPlatform::ResourceProvider m_provider; - QHash m_temp_versions; - ConcurrentTask::Ptr m_hashing_task; - Task::Ptr m_current_task; + QHash m_tempVersions; + Task::Ptr m_hashingTask; + Task::Ptr m_currentTask; + QHash m_updateMetadataTasks; }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 8c85ae122..c3ecccf8e 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -31,6 +31,19 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric }; + +QList modLoaderTypesToList(ModLoaderTypes flags) +{ + QList flagList; + for (auto flag : loaderList) { + if (flags.testFlag(flag)) { + flagList.append(flag); + } + } + return flagList; +} + IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index d5ee12473..523358e4e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -23,7 +23,6 @@ #include #include #include -#include #include class QIODevice; @@ -32,6 +31,7 @@ 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 }; @@ -140,7 +140,7 @@ struct IndexedPack { QString side; bool versionsLoaded = false; - QVector versions; + QList versions; // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index b7364d9ab..62a1ff199 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -73,9 +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; @@ -86,7 +87,7 @@ class ResourceAPI { struct VersionSearchArgs { ModPlatform::IndexedPack pack; - std::optional > mcVersions; + std::optional> mcVersions; std::optional loaders; VersionSearchArgs(VersionSearchArgs const&) = default; @@ -168,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/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp index 678db63cc..c35569d45 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.cpp +++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -43,5 +43,6 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) m.system = Json::ensureBoolean(obj, QString("system"), false); m.description = Json::ensureString(obj, "description", ""); - m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; } diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h index 8d18c671d..187bc05ec 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -18,9 +18,9 @@ #include "ATLPackManifest.h" +#include #include #include -#include namespace ATLauncher { @@ -34,7 +34,7 @@ struct IndexedPack { int position; QString name; PackType type; - QVector versions; + QList versions; bool system; QString description; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index abe7d0177..261ef786d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -69,7 +69,8 @@ PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packNa { m_support = support; m_pack_name = packName; - m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m_pack_safe_name = packName.replace(s_regex, ""); m_version_name = version; m_install_mode = installMode; } @@ -641,22 +642,22 @@ void PackInstallTask::installConfigs() 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(); @@ -678,15 +679,10 @@ void PackInstallTask::extractConfigs() return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); -#else - 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); } @@ -694,7 +690,7 @@ void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); - QVector optionalMods; + QList optionalMods; for (const auto& mod : m_version.mods) { if (mod.optional) { optionalMods.push_back(mod); @@ -702,7 +698,7 @@ void PackInstallTask::downloadMods() } // Select optional mods, if pack contains any - QVector selectedMods; + QList selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); auto mods = m_support->chooseOptionalMods(m_version, optionalMods); @@ -897,13 +893,8 @@ void PackInstallTask::onModsDownloaded() jobPtr.reset(); if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); -#else - m_modExtractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); -#endif connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); @@ -948,7 +939,8 @@ bool PackInstallTask::extractMods(const QMap& toExtract, QString folderToExtract = ""; if (mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; - folderToExtract.remove(QRegularExpression("^/")); + static const QRegularExpression s_regex("^/"); + folderToExtract.remove(s_regex); } qDebug() << "Extracting " + mod.file + " to " + extractToDir; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index ffc358fbb..ce8bb636d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -62,7 +62,7 @@ class UserInteractionSupport { /** * Requests a user interaction to select which optional mods should be installed. */ - virtual std::optional> chooseOptionalMods(PackVersion version, QVector mods) = 0; + virtual std::optional> chooseOptionalMods(const PackVersion& version, QList mods) = 0; /** * Requests a user interaction to select a component version from a given version list diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 8db91087d..b6c3b7a84 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -36,9 +36,9 @@ #pragma once #include +#include #include #include -#include namespace ATLauncher { @@ -113,7 +113,7 @@ struct VersionMod { bool hidden; bool library; QString group; - QVector depends; + QStringList depends; QString colour; QString warning; @@ -139,8 +139,8 @@ struct VersionKeep { }; struct VersionKeeps { - QVector files; - QVector folders; + QList files; + QList folders; }; struct VersionDelete { @@ -149,8 +149,8 @@ struct VersionDelete { }; struct VersionDeletes { - QVector files; - QVector folders; + QList files; + QList folders; }; struct PackVersionMainClass { @@ -171,8 +171,8 @@ struct PackVersion { PackVersionExtraArguments extraArguments; VersionLoader loader; - QVector libraries; - QVector mods; + QList libraries; + QList mods; VersionConfigs configs; QMap colours; diff --git a/launcher/modplatform/atlauncher/ATLShareCode.h b/launcher/modplatform/atlauncher/ATLShareCode.h index 531945bce..9b56c6d7c 100644 --- a/launcher/modplatform/atlauncher/ATLShareCode.h +++ b/launcher/modplatform/atlauncher/ATLShareCode.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include namespace ATLauncher { @@ -32,7 +32,7 @@ struct ShareCodeMod { struct ShareCode { QString pack; QString version; - QVector mods; + QList mods; }; struct ShareCodeResponse { diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 4c2f3d69e..5f812d219 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -20,7 +20,6 @@ #include #include "Json.h" -#include "QObjectPtr.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -33,9 +32,7 @@ static const FlameAPI flameAPI; static ModrinthAPI modrinthAPI; -Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess) - : m_network(network), m_manifest(toProcess) -{} +Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { @@ -87,6 +84,30 @@ void Flame::FileResolvingTask::executeTask() 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); @@ -112,6 +133,8 @@ void Flame::FileResolvingTask::netJobFinished() auto obj = Json::requireObject(file); auto version = FlameMod::loadIndexedPackVersion(obj); auto fileid = version.fileId.toInt(); + Q_ASSERT(fileid != 0); + Q_ASSERT(m_manifest.files.contains(fileid)); m_manifest.files[fileid].version = version; auto url = QUrl(version.downloadUrl, QUrl::TolerantMode); if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) { @@ -144,7 +167,7 @@ void Flame::FileResolvingTask::netJobFinished() << " reason: " << parse_error.errorString(); qWarning() << *m_result; - failed(parse_error.errorString()); + getFlameProjects(); return; } @@ -232,6 +255,10 @@ void Flame::FileResolvingTask::getFlameProjects() 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(); diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index edd9fce9a..3fe8dfb1a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -17,8 +17,6 @@ */ #pragma once -#include - #include "PackManifest.h" #include "tasks/Task.h" @@ -26,7 +24,7 @@ namespace Flame { class FileResolvingTask : public Task { Q_OBJECT public: - explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess); + explicit FileResolvingTask(Flame::Manifest& toProcess); virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } @@ -44,7 +42,6 @@ class FileResolvingTask : public Task { void getFlameProjects(); private: /* data */ - shared_qobject_ptr m_network; Flame::Manifest m_manifest; std::shared_ptr m_result; Task::Ptr m_task; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index ddd48c2b1..15eb7a696 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -102,57 +102,6 @@ QString FlameAPI::getModDescription(int modId) return description; } -QList FlameAPI::getLatestVersions(VersionSearchArgs&& args) -{ - auto versions_url_optional = getVersionsURL(args); - if (!versions_url_optional.has_value()) - return {}; - - auto versions_url = versions_url_optional.value(); - - QEventLoop loop; - - auto netJob = makeShared(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network()); - auto response = std::make_shared(); - QList ver; - - netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - - QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - try { - auto obj = Json::requireObject(doc); - auto arr = Json::requireArray(obj, "data"); - - for (auto file : arr) { - auto file_obj = Json::requireObject(file); - ver.append(FlameMod::loadIndexedPackVersion(file_obj)); - } - - } catch (Json::JsonException& e) { - qCritical() << "Failed to parse response from a version request."; - qCritical() << e.what(); - qDebug() << doc; - } - }); - - QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit); - - netJob->start(); - - loop.exec(); - - return ver; -} - Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); @@ -270,21 +219,44 @@ std::optional FlameAPI::getLatestVersion(QList instanceLoaders, ModPlatform::ModLoaderTypes modLoaders) { - // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update - auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) { - std::optional ver; - for (auto file_tmp : versions) { - if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) { - ver = file_tmp; + 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); } } - return ver; - }; - for (auto l : instanceLoaders) { - auto ver = bestVersion(l); - if (ver.has_value()) { - return ver; + } + // 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 bestVersion(modLoaders); + return {}; } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 3ca0d5448..f85a08eb1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -15,7 +15,6 @@ class FlameAPI : public NetworkResourceAPI { QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); - QList getLatestVersions(VersionSearchArgs&& args); std::optional getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes fallback); @@ -108,12 +107,6 @@ class FlameAPI : public NetworkResourceAPI { 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 { auto addonId = args.pack.addonId.toString(); @@ -129,6 +122,11 @@ class FlameAPI : public NetworkResourceAPI { return url; } + private: + [[nodiscard]] std::optional getInfoURL(QString const& id) const override + { + return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { auto addonId = args.dependency.addonId.toString(); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 370f37c5f..047813675 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -3,115 +3,31 @@ #include "FlameAPI.h" #include "FlameModIndex.h" -#include +#include #include #include "Json.h" +#include "QObjectPtr.h" #include "ResourceDownloadTask.h" -#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" +#include "modplatform/ModIndex.h" #include "net/ApiDownload.h" +#include "net/NetJob.h" +#include "tasks/Task.h" static FlameAPI api; bool FlameCheckUpdate::abort() { - m_was_aborted = true; - if (m_net_job) - return m_net_job->abort(); - return true; -} - -ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info) -{ - ModPlatform::IndexedPack pack; - - QEventLoop loop; - - auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); - - auto response = std::make_shared(); - auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); - auto dl = Net::ApiDownload::makeByteArray(url, response); - get_project_job->addNetAction(dl); - - QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - try { - auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::requireObject(doc_obj, "data"); - FlameMod::loadIndexedPack(pack, data_obj); - } catch (Json::JsonException& e) { - qWarning() << e.cause(); - qDebug() << doc; - } - }); - - 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(); - }); - - get_project_job->start(); - loop.exec(); - - return pack; -} - -ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId) -{ - ModPlatform::IndexedVersion ver; - - QEventLoop loop; - - auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); - - auto response = std::make_shared(); - auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); - auto dl = Net::ApiDownload::makeByteArray(url, response); - get_file_info_job->addNetAction(dl); - - QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - try { - auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::requireObject(doc_obj, "data"); - ver = FlameMod::loadIndexedPackVersion(data_obj); - } catch (Json::JsonException& e) { - qWarning() << e.cause(); - 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(); - }); - - get_file_info_job->start(); - loop.exec(); - - return ver; + bool result = false; + if (m_task && m_task->canAbort()) { + result = m_task->abort(); + } + Task::abort(); + return result; } /* Check for update: @@ -121,62 +37,165 @@ ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileI * */ void FlameCheckUpdate::executeTask() { - setStatus(tr("Preparing mods for CurseForge...")); + setStatus(tr("Preparing resources for CurseForge...")); - int i = 0; - for (auto* mod : m_mods) { - setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); - setProgress(i++, m_mods.size()); + auto netJob = new NetJob("Get latest versions", APPLICATION->network()); + connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); - auto latest_vers = api.getLatestVersions({ { mod->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, mod->loaders()); - - setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name())); - - if (!latest_ver.has_value() || !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.")); + connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); + for (auto* resource : m_resources) { + auto versions_url_optional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_game_versions }); + if (!versions_url_optional.has_value()) continue; - } - if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != mod->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(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url); + auto response = std::make_shared(); + auto task = Net::ApiDownload::makeByteArray(versions_url_optional.value(), response); - continue; - } + connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); }); + netJob->addNetAction(task); + } + m_task.reset(netJob); + m_task->start(); +} - // 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::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; - } - - auto download_task = makeShared(pack, latest_ver.value(), 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, mod->enabled()); - } - m_deps.append(std::make_shared(pack, latest_ver.value())); +void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr response) +{ + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; } - emitSucceeded(); + // 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::FLAME; + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + FlameMod::loadIndexedPackVersions(*pack.get(), arr); + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + auto latest_ver = api.getLatestVersion(pack->versions, m_loaders_list, resource->metadata()->loaders); + + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); + + 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); + return; + } + + if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { + m_blocked[resource] = latest_ver->fileId.toString(); + return; + } + + 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.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.value())); } + +void FlameCheckUpdate::collectBlockedMods() +{ + QStringList addonIds; + QHash quickSearch; + for (auto const& resource : m_blocked.keys()) { + auto addonId = resource->metadata()->project_id.toString(); + addonIds.append(addonId); + quickSearch[addonId] = resource; + } + + auto response = std::make_shared(); + Task::Ptr projTask; + + if (addonIds.isEmpty()) { + emitSucceeded(); + return; + } else if (addonIds.size() == 1) { + projTask = api.getProject(*addonIds.begin(), response); + } else { + projTask = api.getProjects(addonIds, response); + } + + connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + + auto resource = quickSearch.find(id).value(); + + ModPlatform::IndexedPack pack; + try { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); + + FlameMod::loadIndexedPack(pack, entry_obj); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); + emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), + recover_url); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error + connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); + m_task.reset(projTask); + m_task->start(); +} \ No newline at end of file diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index e30ae35b9..eb80ce47c 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -1,17 +1,16 @@ #pragma once #include "modplatform/CheckUpdateTask.h" -#include "net/NetJob.h" class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(QList& mods, + FlameCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loadersList, mods_folder) + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) {} public slots: @@ -19,12 +18,12 @@ class FlameCheckUpdate : public CheckUpdateTask { protected slots: void executeTask() override; + private slots: + void getLatestVersionCallback(Resource* resource, std::shared_ptr response); + void collectBlockedMods(); private: - ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info); - ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId); + Task::Ptr m_task = nullptr; - NetJob* m_net_job = nullptr; - - bool m_was_aborted = false; + QHash m_blocked; }; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index b8c40ee42..c30ba5249 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -36,7 +36,7 @@ #include "FlameInstanceCreationTask.h" #include "QObjectPtr.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -75,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(); } @@ -227,17 +227,22 @@ bool FlameCreationTask::updateInstance() 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)); + if (relative_path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9))); + } else { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled")); + } } }); 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."), @@ -386,7 +391,8 @@ bool FlameCreationTask::createInstance() // Hack to correct some 'special sauce'... if (mcVersion.endsWith('.')) { - mcVersion.remove(QRegularExpression("[.]+$")); + static const QRegularExpression s_regex("[.]+$"); + mcVersion.remove(s_regex); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } @@ -430,25 +436,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(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(); @@ -467,14 +474,14 @@ 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.version.fileName.endsWith(".zip")) { - m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder)); + if (result.resourceType != PackedResourceType::Mod) { + m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } if (result.version.downloadUrl.isEmpty()) { @@ -506,7 +513,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { - m_mod_id_resolver.reset(); + m_modIdResolver.reset(); setError("Canceled"); loop.quit(); } @@ -517,8 +524,8 @@ 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) { @@ -553,26 +560,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) if (!result.version.downloadUrl.isEmpty()) { qDebug() << "Will download" << result.version.downloadUrl << "to" << path; auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); - m_files_job->addNetAction(dl); + m_filesJob->addNetAction(dl); } } - connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() { - m_files_job.reset(); - validateZIPResources(loop); + 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_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 @@ -596,8 +603,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++; @@ -607,11 +620,11 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResources(QEventLoop& loop) +void FlameCreationTask::validateOtherResources(QEventLoop& loop) { - qDebug() << "Validating whether resources stored as .zip are in the right place"; + qDebug() << "Validating whether other resources are in the right place"; QStringList zipMods; - for (auto [fileName, targetFolder] : m_ZIP_resources) { + for (auto [fileName, targetFolder] : m_otherResources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); @@ -671,21 +684,23 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop) installWorld(worldPath); break; case PackedResourceType::UNKNOWN: + /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; break; } } - auto task = makeShared(this, "CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); - auto results = m_mod_id_resolver->getResults().files; + // 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)); + task->addTask(makeShared(folder, file.pack, file.version)); } connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); - m_process_update_file_info_job = task; + m_processUpdateFileInfoJob = task; task->start(); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 28ab176c2..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(QEventLoop& loop); + 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 7de05f177..c1b9e67af 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -76,12 +76,9 @@ static QString enumToString(int hash_algorithm) } } -void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - [[maybe_unused]] const shared_qobject_ptr& network, - const BaseInstance* inst) +void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -105,9 +102,6 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); - if (versionArray.isEmpty()) { - return {}; - } ModPlatform::IndexedVersion file; for (auto mcVer : versionArray) { @@ -214,7 +208,7 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); auto loaders = profile->getSupportedModLoaders(); - QVector versions; + QList versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 1bcaa44ba..f6b4b22be 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -6,7 +6,6 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" namespace FlameMod { @@ -14,10 +13,7 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - const shared_qobject_ptr& network, - const BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); +ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); +ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst); } // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index d661f1f05..c9a35f3cf 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -47,7 +47,7 @@ FlamePackExportTask::FlamePackExportTask(const QString& name, bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFileFunction filter) : name(name) , version(version) , author(author) @@ -70,7 +70,6 @@ bool FlamePackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -103,8 +102,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()); @@ -172,6 +170,7 @@ void FlamePackExportTask::collectHashes() progressStep->status = status; stepProgress(*progressStep); }); + connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); hashingTask->start(); } @@ -247,6 +246,7 @@ void FlamePackExportTask::makeApiRequest() getProjectsInfo(); }); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task->start(); } @@ -325,6 +325,7 @@ void FlamePackExportTask::getProjectsInfo() buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index b11eb17fa..38acdf518 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -34,7 +34,7 @@ class FlamePackExportTask : public Task { bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFileFunction filter); protected: void executeTask() override; @@ -51,7 +51,7 @@ class FlamePackExportTask : public Task { MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; - const MMCZip::FilterFunction filter; + const MMCZip::FilterFileFunction filter; struct ResolvedFile { int addonId; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 8c25b0482..8a7734be5 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -77,7 +77,7 @@ void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto version = Json::requireObject(versionIter); Flame::IndexedVersion file; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 11633deee..30391288b 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "modplatform/ModIndex.h" namespace Flame { @@ -39,7 +38,7 @@ struct IndexedPack { QString logoUrl; bool versionsLoaded = false; - QVector versions; + QList versions; bool extraInfoLoaded = false; ModpackExtra extra; diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index e576a6a84..278105f4a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -45,7 +45,7 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) Flame::File file; loadFileV1(file, obj); - + Q_ASSERT(file.projectId != 0); pack.files.insert(file.fileId, file); } diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 49a0b2d68..ebb3ed5cc 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -36,10 +36,11 @@ #pragma once #include +#include #include #include #include -#include +#include "minecraft/mod/tasks/LocalResourceParse.h" #include "modplatform/ModIndex.h" namespace Flame { @@ -54,6 +55,7 @@ struct File { // our QString targetFolder = QStringLiteral("mods"); + PackedResourceType resourceType; }; struct Modloader { @@ -64,7 +66,7 @@ struct Modloader { struct Minecraft { QString version; QString libraries; - QVector modLoaders; + QList modLoaders; }; struct Manifest { diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp index aea16ab50..678f89917 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); } @@ -65,7 +65,7 @@ QString toMarkdown(QList mods, OptionalData extraData) auto meta = mod->metadata(); 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); } @@ -95,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); } @@ -124,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; } @@ -156,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) @@ -203,7 +203,8 @@ QString exportToModList(QList mods, QString lineTemplate) for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); - auto url = mod->metaurl(); + auto modID = mod->mod_id(); + auto url = mod->homepage(); auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); @@ -211,6 +212,7 @@ QString exportToModList(QList mods, QString lineTemplate) auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) + .replace("{mod_id}", modID) .replace("{url}", url) .replace("{version}", ver) .replace("{authors}", authors) diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index 974e732a7..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](const QString& reason) { + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); - + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); @@ -102,11 +107,17 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi callbacks.on_succeed(doc, args.pack); }); - QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); @@ -141,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) { @@ -153,11 +164,17 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, callbacks.on_succeed(doc, args.dependency); }); - QObject::connect(netJob.get(), &NetJob::failed, [netJob, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } callbacks.on_fail(reason, network_error_code); }); return netJob; diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index a0beeddcc..aea9cefad 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -79,7 +79,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); - foreach (Modpack currentPack, packs) { + for (auto& currentPack : packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index e37d949d5..4c7a8f6aa 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -40,7 +40,7 @@ class PackFetchTask : public QObject { void failed(QString reason); void aborted(); - void privateFileDownloadFinished(Modpack modpack); + void privateFileDownloadFinished(const Modpack& modpack); void privateFileDownloadFailed(QString reason, QString packCode); }; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 7157f7f2d..2b9bd127a 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -52,7 +52,7 @@ namespace LegacyFTB { -PackInstallTask::PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version) +PackInstallTask::PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version) { m_pack = pack; m_version = version; @@ -108,13 +108,8 @@ void PackInstallTask::unzip() return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); -#else - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); -#endif connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); @@ -138,7 +133,7 @@ void PackInstallTask::install() if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { - emitFailed(tr("Failed to move unzipped Minecraft!")); + emitFailed(tr("Failed to move unpacked Minecraft!")); return; } } @@ -165,7 +160,7 @@ void PackInstallTask::install() // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); - foreach (const QJsonValue& value, libs) { + for (const auto& value : libs) { QString nameValue = value.toObject().value("name").toString(); if (!nameValue.startsWith("net.minecraftforge")) { continue; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index 30ff48597..42808a1a2 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -18,7 +18,7 @@ class PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version); + explicit PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version); virtual ~PackInstallTask() {} bool canAbort() const override { return true; } diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp index 2ae351329..17e9f7d76 100644 --- a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp +++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -44,12 +44,8 @@ namespace LegacyFTB { void PrivatePackManager::load() { try { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); currentPacks = QSet(foo.begin(), foo.end()); -#else - currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet(); -#endif dirty = false; } catch (...) { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 323711e02..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); } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 070f59dad..3e9c4befd 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -12,21 +12,21 @@ class ModrinthAPI : public NetworkResourceAPI { public: - auto currentVersion(QString hash, QString hash_format, std::shared_ptr response) -> Task::Ptr; + Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr response); - auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) -> Task::Ptr; + Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response); - auto latestVersion(QString hash, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) -> Task::Ptr; + Task::Ptr latestVersion(QString hash, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response); - auto latestVersions(const QStringList& hashes, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) -> Task::Ptr; + Task::Ptr latestVersions(const QStringList& hashes, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + std::shared_ptr response); Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; @@ -71,16 +71,33 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getSideFilters(QString side) -> const QString { - if (side.isEmpty() || side == "both") { + 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\""); + 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\""); + 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) { @@ -116,6 +133,8 @@ class ModrinthAPI : public NetworkResourceAPI { } 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))); @@ -170,7 +189,7 @@ class ModrinthAPI : public NetworkResourceAPI { { 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; @@ -187,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 70bf138a8..aa371f280 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -8,20 +8,13 @@ #include "QObjectPtr.h" #include "ResourceDownloadTask.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" static ModrinthAPI api; -ModrinthCheckUpdate::ModrinthCheckUpdate(QList& mods, - std::list& mcVersions, - QList loadersList, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loadersList, mods_folder) - , m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) -{} - bool ModrinthCheckUpdate::abort() { if (m_job) @@ -36,24 +29,24 @@ bool ModrinthCheckUpdate::abort() * */ void ModrinthCheckUpdate::executeTask() { - setStatus(tr("Preparing mods for Modrinth...")); - setProgress(0, 9); + setStatus(tr("Preparing resources for Modrinth...")); + setProgress(0, (m_loaders_list.isEmpty() ? 1 : m_loaders_list.length()) * 2 + 1); auto hashing_task = - makeShared(this, "MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); - for (auto* mod : m_mods) { - auto hash = mod->metadata()->hash; + 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 != m_hash_type) { - auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_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); } else { - m_mappings.insert(hash, mod); + m_mappings.insert(hash, resource); } } @@ -62,10 +55,28 @@ void ModrinthCheckUpdate::executeTask() hashing_task->start(); } -void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, - ModPlatform::ModLoaderTypes loader, - bool forceModLoaderCheck) +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(); + QStringList hashes = m_mappings.keys(); + auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); + + connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); + + connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); + + m_job = job; + job->start(); +} + +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) { @@ -77,31 +88,28 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp return; } - setStatus(tr("Parsing the API response from Modrinth...")); - setProgress(m_next_loader_idx * 2, 9); - try { - for (auto hash : m_mappings.keys()) { - if (forceModLoaderCheck && !(m_mappings[hash]->loaders() & loader)) { - continue; - } + 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; - static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, - ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric }; - for (auto flag : flags) { - if (loader.testFlag(flag)) { + if (loader.has_value()) { + for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) { loader_filter = ModPlatform::getModLoaderAsString(flag); break; } @@ -116,106 +124,72 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp 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; } - auto mod_iter = m_mappings.find(hash); - if (mod_iter == m_mappings.end()) { - qCritical() << "Failed to remap mod from Modrinth!"; - continue; - } - auto mod = *mod_iter; - m_mappings.remove(hash); - - 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->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; - if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) { - if (mod->version() == project_ver.version_number) - continue; + if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { + auto download_task = makeShared(pack, project_ver, m_resource_model); - auto download_task = makeShared(pack, project_ver, m_mods_folder); + 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_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type, - project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, mod->enabled()); + 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()); + emitFailed(e.cause() + ": " + e.what()); return; } checkNextLoader(); } -void ModrinthCheckUpdate::getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck) -{ - auto response = std::make_shared(); - QStringList hashes; - if (forceModLoaderCheck) { - for (auto hash : m_mappings.keys()) { - if (m_mappings[hash]->loaders() & loader) { - hashes.append(hash); - } - } - } else { - hashes = m_mappings.keys(); - } - auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); - - connect(job.get(), &Task::succeeded, this, - [this, response, loader, forceModLoaderCheck] { checkVersionsResponse(response, loader, forceModLoaderCheck); }); - - connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); - - setStatus(tr("Waiting for the API response from Modrinth...")); - setProgress(m_next_loader_idx * 2 - 1, 9); - - m_job = job; - job->start(); -} - void ModrinthCheckUpdate::checkNextLoader() { if (m_mappings.isEmpty()) { emitSucceeded(); return; } - if (m_next_loader_idx < m_loaders_list.size()) { - getUpdateModsForLoader(m_loaders_list.at(m_next_loader_idx)); - m_next_loader_idx++; + + if (m_loaders_list.isEmpty() && m_loader_idx == 0) { + getUpdateModsForLoader({}); + m_loader_idx++; return; } - static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, ModPlatform::ModLoaderType::Quilt, - ModPlatform::ModLoaderType::Fabric }; - for (auto flag : flags) { - if (!m_loaders_list.contains(flag)) { - m_loaders_list.append(flag); - m_next_loader_idx++; - setProgress(m_next_loader_idx * 2 - 1, 9); - for (auto m : m_mappings) { - if (m->loaders() & flag) { - getUpdateModsForLoader(flag, true); - return; - } - } - setProgress(m_next_loader_idx * 2, 9); - } + + if (m_loader_idx < m_loaders_list.size()) { + getUpdateModsForLoader(m_loaders_list.at(m_loader_idx)); + m_loader_idx++; + return; } - for (auto m : m_mappings) { - emit checkFailed(m, - tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + + 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(); - return; } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index dab4bda2f..204b24784 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -6,23 +6,26 @@ class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - ModrinthCheckUpdate(QList& mods, + ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder); + 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: bool abort() override; protected slots: void executeTask() override; - void getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); - void checkVersionsResponse(std::shared_ptr response, ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); + void getUpdateModsForLoader(std::optional loader); + void checkVersionsResponse(std::shared_ptr response, std::optional loader); void checkNextLoader(); private: Task::Ptr m_job = nullptr; - QHash m_mappings; + QHash m_mappings; QString m_hash_type; - int m_next_loader_idx = 0; + int m_loader_idx = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 994dd774f..374b7681e 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -121,6 +121,11 @@ bool ModrinthCreationTask::updateInstance() continue; qDebug() << "Scheduling" << file.path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); + if (file.path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9))); + } else { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled")); + } } } @@ -243,7 +248,8 @@ bool ModrinthCreationTask::createInstance() auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); - QHash mods; + // TODO make this work with other sorts of resource + QHash resources; for (auto file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); @@ -259,14 +265,16 @@ bool ModrinthCreationTask::createInstance() ModDetails d; d.mod_id = file_path; mod->setDetails(d); - mods[file.hash.toHex()] = mod; + 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)); downloadMods->addNetAction(dl); - if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) @@ -283,13 +291,13 @@ bool ModrinthCreationTask::createInstance() bool ended_well = false; - connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); - connect(downloadMods.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(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); - connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(downloadMods.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); @@ -302,18 +310,18 @@ bool ModrinthCreationTask::createInstance() loop.exec(); if (!ended_well) { - for (auto m : mods) { - delete m; + for (auto resource : resources) { + delete resource; } return ended_well; } QEventLoop ensureMetaLoop; QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); - auto ensureMetadataTask = makeShared(mods, folder, ModPlatform::ResourceProvider::MODRINTH); - connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; }); + 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, [&](qint64 current, qint64 total) { + connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); @@ -323,10 +331,10 @@ bool ModrinthCreationTask::createInstance() m_task = ensureMetadataTask; ensureMetaLoop.exec(); - for (auto m : mods) { - delete m; + for (auto resource : resources) { + delete resource; } - mods.clear(); + resources.clear(); // Update information of the already installed instance, if any. if (m_instance && ended_well) { @@ -413,23 +421,30 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } if (!optionalFiles.empty()) { - QStringList oFiles; - for (auto file : optionalFiles) - oFiles.push_back(file.path); - OptionalModDialog optionalModDialog(m_parent, oFiles); - if (optionalModDialog.exec() == QDialog::Rejected) { - emitAborted(); - return false; - } - - auto selectedMods = optionalModDialog.getResult(); - for (auto file : optionalFiles) { - if (selectedMods.contains(file.path)) { - file.required = true; - } else { - file.path += ".disabled"; + if (show_optional_dialog) { + QStringList oFiles; + for (auto file : optionalFiles) + oFiles.push_back(file.path); + OptionalModDialog optionalModDialog(m_parent, oFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + return false; + } + + auto selectedMods = optionalModDialog.getResult(); + for (auto file : optionalFiles) { + if (selectedMods.contains(file.path)) { + file.required = true; + } else { + file.path += ".disabled"; + } + files.push_back(file); + } + } else { + for (auto file : optionalFiles) { + file.path += ".disabled"; + files.push_back(file); } - files.push_back(file); } } if (set_internal_data) { diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index b7c2757e5..4b19acd3f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -40,7 +40,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFileFunction filter) : name(name) , version(version) , summary(summary) @@ -63,7 +63,6 @@ bool ModrinthPackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -123,7 +122,7 @@ void ModrinthPackExportTask::collectHashes() 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"; @@ -158,6 +157,7 @@ void ModrinthPackExportTask::makeApiRequest() task = api.currentVersions(pendingHashes.values(), "sha512", response); connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); task->start(); } } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index ee740a456..ec4730de5 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -35,7 +35,7 @@ class ModrinthPackExportTask : public Task { bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFileFunction filter); protected: void executeTask() override; @@ -58,7 +58,7 @@ class ModrinthPackExportTask : public Task { MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; - const MMCZip::FilterFunction filter; + const MMCZip::FilterFileFunction filter; ModrinthAPI api; QFileInfoList files; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 48b27a597..744b058c0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -112,9 +112,9 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraDataLoaded = true; } -void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) +void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); @@ -131,9 +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; @@ -145,7 +143,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, 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) { @@ -247,15 +245,15 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, 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"); auto loaders = profile->getSupportedModLoaders(); - QVector versions; + QList versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 93f91eec2..16f3d262c 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -19,14 +19,13 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index c52a1743b..be565bf11 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; @@ -102,7 +99,7 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) { - QVector unsortedVersions; + QList unsortedVersions; auto arr = Json::requireArray(doc); @@ -134,6 +131,7 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion 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) { diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 2e5e2da84..97b8ab712 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -40,10 +40,10 @@ #include #include +#include #include #include #include -#include #include "modplatform/ModIndex.h" @@ -110,7 +110,7 @@ struct Modpack { bool extraInfoLoaded = false; ModpackExtra extra; - QVector versions; + QList versions; }; void loadIndexedPack(Modpack&, QJsonObject&); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 272900f0e..a3bb74399 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -35,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)); @@ -90,7 +90,7 @@ auto intEntry(toml::table table, QString entry_name) -> int return node.value_or(0); } -auto V1::createModFormat([[maybe_unused]] QDir& index_dir, +auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { @@ -119,10 +119,14 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, 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) }; @@ -134,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!"); @@ -186,11 +190,8 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) } toml::array loaders; - for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric, - ModPlatform::Quilt }) { - if (mod.loaders & loader) { - loaders.push_back(getModLoaderAsString(loader).toStdString()); - } + for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) { + loaders.push_back(getModLoaderAsString(loader).toStdString()); } toml::array mcVersions; for (auto version : mod.mcVersions) { @@ -211,6 +212,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) { "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() }, @@ -228,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); @@ -247,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); @@ -259,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; @@ -295,7 +297,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = stringToSide(stringEntry(table, "side")); - mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "x-prismlauncher-release-type")); + 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()) { @@ -315,6 +317,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.mcVersions.sort(); } } + mod.version_number = table["x-prismlauncher-version-number"].value_or(""); { // [download] info auto download_table = table["download"].as_table(); @@ -356,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 95362bbfe..44896e74c 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -32,11 +32,13 @@ 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{}; @@ -56,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. @@ -70,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/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h index 1a06d7037..3a5947515 100644 --- a/launcher/modplatform/technic/SolderPackManifest.h +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -19,15 +19,15 @@ #pragma once #include +#include #include -#include namespace TechnicSolder { struct Pack { QString recommended; QString latest; - QVector builds; + QList builds; }; void loadPack(Pack& v, QJsonObject& obj); @@ -41,7 +41,7 @@ struct PackBuildMod { struct PackBuild { QString minecraft; - QVector mods; + QList mods; }; void loadPackBuild(PackBuild& v, QJsonObject& obj); diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 95c1a8f44..3a58a4667 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -54,7 +54,7 @@ Task::State FileSink::init(QNetworkRequest& request) return Task::State::Failed; } - wroteAnyData = false; + m_wroteAnyData = false; m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; @@ -72,17 +72,19 @@ Task::State FileSink::write(QByteArray& data) qCCritical(taskNetLogC) << "Failed writing into " + m_filename; m_output_file->cancelWriting(); m_output_file.reset(); - wroteAnyData = false; + m_wroteAnyData = false; return Task::State::Failed; } - wroteAnyData = true; + m_wroteAnyData = true; return Task::State::Running; } Task::State FileSink::abort() { - m_output_file->cancelWriting(); + if (m_output_file) { + m_output_file->cancelWriting(); + } failAllValidators(); return Task::State::Failed; } @@ -100,7 +102,7 @@ Task::State FileSink::finalize(QNetworkReply& reply) // if we wrote any data to the save file, we try to commit the data to the real file. // if it actually got a proper file, we write it even if it was empty - if (gotFile || wroteAnyData) { + if (gotFile || m_wroteAnyData) { // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits if (!finalizeAllValidators(reply)) diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 272f8ddc3..67c25361c 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -58,7 +58,7 @@ class FileSink : public Sink { protected: QString m_filename; - bool wroteAnyData = false; + bool m_wroteAnyData = false; std::unique_ptr m_output_file; }; } // namespace Net diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 4985ad080..77c1de47d 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -166,8 +166,10 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool return true; } -void HttpMetaCache::evictAll() +// returns true on success, false otherwise +auto HttpMetaCache::evictAll() -> bool { + bool ret = true; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; @@ -176,8 +178,10 @@ void HttpMetaCache::evictAll() qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); - FS::deletePath(map.base_path); + // AND all return codes together so the result is true iff all runs of deletePath() are true + ret &= FS::deletePath(map.base_path); } + return ret; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 036a8dd94..144012ae5 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -113,7 +113,7 @@ class HttpMetaCache : public QObject { // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; - void evictAll(); + bool evictAll(); void addBase(QString base, QString base_root); diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 889588a11..8896f10e3 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -78,7 +78,7 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) { QFileInfo output_file_info(m_filename); - if (wroteAnyData) { + if (m_wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } @@ -98,8 +98,8 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) auto cache_control_header = reply.rawHeader("Cache-Control"); qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; - QRegularExpression max_age_expr("max-age=([0-9]+)"); - qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong(); + static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); + qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); m_entry->setMaximumAge(max_age); } else if (reply.hasRawHeader("Expires")) { diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 3cd3958f7..335e360b2 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -45,7 +45,7 @@ #endif NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) - : ConcurrentTask(nullptr, job_name), m_network(network) + : ConcurrentTask(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && max_concurrent < 0) diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index cf2e72858..ef533f599 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -80,7 +80,7 @@ void NetRequest::executeTask() emit finished(); return; case State::Running: - qCDebug(logCat) << getUid().toString() << "Runninng " << m_url.toString(); + qCDebug(logCat) << getUid().toString() << "Running " << m_url.toString(); break; case State::Inactive: case State::Failed: @@ -104,12 +104,10 @@ void NetRequest::executeTask() header_proxy->writeHeaders(request); } -#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(); @@ -122,11 +120,7 @@ void NetRequest::executeTask() 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); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &NetRequest::downloadError); -#endif connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } @@ -323,11 +317,7 @@ auto NetRequest::abort() -> bool { m_state = State::AbortedByUser; if (m_reply) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); -#else - disconnect(m_reply.get(), QOverload::of(&QNetworkReply::error), nullptr, nullptr); -#endif m_reply->abort(); } return true; diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index c67d3b23c..86a44669e 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -130,11 +130,7 @@ void PasteUpload::executeTask() connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &PasteUpload::downloadError); -#endif m_reply = std::shared_ptr(rep); diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h index 689f11979..d8d36d2c3 100644 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ b/launcher/pathmatcher/FSTreeMatcher.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "IPathMatcher.h" class FSTreeMatcher : public IPathMatcher { diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h index 3e2bdb95d..3ad07b643 100644 --- a/launcher/pathmatcher/MultiMatcher.h +++ b/launcher/pathmatcher/MultiMatcher.h @@ -1,5 +1,6 @@ +#pragma once + #include -#include #include "IPathMatcher.h" class MultiMatcher : public IPathMatcher { diff --git a/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h index a6a3e616d..e36516386 100644 --- a/launcher/pathmatcher/RegexpMatcher.h +++ b/launcher/pathmatcher/RegexpMatcher.h @@ -1,3 +1,5 @@ +#pragma once + #include #include "IPathMatcher.h" @@ -10,6 +12,8 @@ class RegexpMatcher : public IPathMatcher { m_onlyFilenamePart = !regexp.contains('/'); } + RegexpMatcher(const QRegularExpression& regex) : m_regexp(regex) { m_onlyFilenamePart = !regex.pattern().contains('/'); } + RegexpMatcher& caseSensitive(bool cs = true) { if (cs) { diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h index ff3805179..57bf63a30 100644 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ b/launcher/pathmatcher/SimplePrefixMatcher.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -#include #include "IPathMatcher.h" class SimplePrefixMatcher : public IPathMatcher { diff --git a/launcher/qtlogging.ini b/launcher/qtlogging.ini index c12d1e109..10f724163 100644 --- a/launcher/qtlogging.ini +++ b/launcher/qtlogging.ini @@ -3,6 +3,9 @@ # prevent log spam and strange bugs # qt.qpa.drawing in particular causes theme artifacts on MacOS qt.*.debug=false +# supress image format noise +kf.imageformats.plugins.hdr=false +kf.imageformats.plugins.xcf=false # don't log credentials by default launcher.auth.credentials.debug=false # remove the debug lines, other log levels still get through 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 489d1d5a2..007efcde3 100644 --- a/launcher/resources/documents/documents.qrc +++ b/launcher/resources/documents/documents.qrc @@ -2,7 +2,6 @@ ../../../COPYING.md - login-qr.svg diff --git a/launcher/resources/documents/login-qr.svg b/launcher/resources/documents/login-qr.svg deleted file mode 100644 index 1b88e3c83..000000000 --- a/launcher/resources/documents/login-qr.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - 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/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/shaders/fshader.glsl b/launcher/resources/shaders/fshader.glsl new file mode 100644 index 000000000..d6a93db5d --- /dev/null +++ b/launcher/resources/shaders/fshader.glsl @@ -0,0 +1,20 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/fshader.glsl +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +void main() +{ + // Set fragment color from texture + vec4 texColor = texture2D(texture, v_texcoord); + if (texColor.a < 0.1) discard; // Optional: Discard fully transparent pixels + gl_FragColor = texColor; +} diff --git a/launcher/resources/shaders/shaders.qrc b/launcher/resources/shaders/shaders.qrc new file mode 100644 index 000000000..835e0fea7 --- /dev/null +++ b/launcher/resources/shaders/shaders.qrc @@ -0,0 +1,6 @@ + + + vshader.glsl + fshader.glsl + + \ No newline at end of file diff --git a/launcher/resources/shaders/vshader.glsl b/launcher/resources/shaders/vshader.glsl new file mode 100644 index 000000000..2d5e2db30 --- /dev/null +++ b/launcher/resources/shaders/vshader.glsl @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/vshader.glsl +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; +uniform mat4 model_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +void main() +{ + // Calculate vertex position in screen space + gl_Position = mvp_matrix * model_matrix * a_position; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} 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/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp index 1e5dce251..0df22b42d 100644 --- a/launcher/settings/SettingsObject.cpp +++ b/launcher/settings/SettingsObject.cpp @@ -124,3 +124,8 @@ void SettingsObject::connectSignals(const Setting& setting) connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); connect(&setting, SIGNAL(settingReset(Setting)), this, SIGNAL(settingReset(const Setting&))); } + +std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) +{ + return contains(id) ? getSetting(id) : registerSetting(id, defVal); +} diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index f133f2f7f..bd3f71b36 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -103,6 +103,16 @@ class SettingsObject : public QObject { */ std::shared_ptr getSetting(const QString& id) const; + /*! + * \brief Gets the setting with the given ID. + * \brief if is not registered yet it does that + * \param id The ID of the setting to get. + * \return A pointer to the setting with the given ID. + * Returns null if there is no setting with the given ID. + * \sa operator []() + */ + std::shared_ptr getOrRegisterSetting(const QString& id, QVariant defVal = QVariant()); + /*! * \brief Gets the value of the setting with the given ID. * \param id The ID of the setting to get. diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index f2ee40c31..ad2a14c42 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -38,7 +38,7 @@ #include #include "tasks/Task.h" -ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent) : Task(parent), m_total_max_size(max_concurrent) +ConcurrentTask::ConcurrentTask(QString task_name, int max_concurrent) : Task(), m_total_max_size(max_concurrent) { setObjectName(task_name); } diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index 0d6709940..cc6256cf8 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -43,12 +43,16 @@ #include "tasks/Task.h" +/*! + * Runs a list of tasks concurrently (according to `max_concurrent` parameter). + * Behaviour is the same as regular Task (e.g. starts using start()) + */ class ConcurrentTask : public Task { Q_OBJECT 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 @@ -59,6 +63,7 @@ class ConcurrentTask : public Task { inline auto isMultiStep() const -> bool override { return totalSize() > 1; } auto getStepProgress() const -> TaskStepProgressList override; + //! Adds a task to execute in this ConcurrentTask void addTask(Task::Ptr task); public slots: diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp index 5afe03964..ba0c23542 100644 --- a/launcher/tasks/MultipleOptionsTask.cpp +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -36,7 +36,7 @@ #include -MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : ConcurrentTask(parent, task_name, 1) {} +MultipleOptionsTask::MultipleOptionsTask(const QString& task_name) : ConcurrentTask(task_name, 1) {} void MultipleOptionsTask::executeNextSubTask() { diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h index 9a88a9999..7a19ed6ad 100644 --- a/launcher/tasks/MultipleOptionsTask.h +++ b/launcher/tasks/MultipleOptionsTask.h @@ -42,7 +42,7 @@ class MultipleOptionsTask : public ConcurrentTask { Q_OBJECT public: - explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = ""); + explicit MultipleOptionsTask(const QString& task_name = ""); ~MultipleOptionsTask() override = default; private slots: diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 509d91cf7..2e48414f2 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -38,7 +38,7 @@ #include #include "tasks/ConcurrentTask.h" -SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {} +SequentialTask::SequentialTask(QString task_name) : ConcurrentTask(task_name, 1) {} void SequentialTask::subTaskFailed(Task::Ptr task, const QString& msg) { diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index a7c101ab4..77cd4387f 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -47,7 +47,7 @@ class SequentialTask : public ConcurrentTask { Q_OBJECT public: - explicit SequentialTask(QObject* parent = nullptr, QString task_name = ""); + explicit SequentialTask(QString task_name = ""); ~SequentialTask() override = default; protected slots: diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index b17096ca7..1871c5df8 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -40,7 +40,7 @@ Q_LOGGING_CATEGORY(taskLogC, "launcher.task") -Task::Task(QObject* parent, bool show_debug) : QObject(parent), m_show_debug(show_debug) +Task::Task(bool show_debug) : m_show_debug(show_debug) { m_uid = QUuid::createUuid(); setAutoDelete(false); diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 883408c97..fcd075150 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -79,6 +79,12 @@ Q_DECLARE_METATYPE(TaskStepProgress) using TaskStepProgressList = QList>; +/*! + * Represents a task that has to be done. + * To create a task, you need to subclass this class, implement the executeTask() method and call + * emitSucceeded() or emitFailed() when the task is done. + * the caller needs to call start() to start the task. + */ class Task : public QObject, public QRunnable { Q_OBJECT public: @@ -87,7 +93,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; @@ -130,23 +136,27 @@ class Task : public QObject, public QRunnable { signals: void started(); void progress(qint64 current, qint64 total); + //! called when a task has either succeeded, aborted or failed. void finished(); + //! called when a task has succeeded void succeeded(); + //! called when a task has been aborted by calling abort() void aborted(); void failed(QString reason); void status(QString status); void details(QString details); void stepProgress(TaskStepProgress const& task_progress); - /** Emitted when the canAbort() status has changed. - */ + //! Emitted when the canAbort() status has changed. */ void abortStatusChanged(bool can_abort); public slots: // QRunnable's interface void run() override { start(); } + //! used by the task caller to start the task virtual void start(); + //! used by external code to ask the task to abort virtual bool abort() { if (canAbort()) @@ -161,11 +171,16 @@ class Task : public QObject, public QRunnable { } protected: + //! The task subclass must implement this method. This method is called to start to run the task. + //! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead. virtual void executeTask() = 0; protected slots: + //! The Task subclass must call this method when the task has succeeded virtual void emitSucceeded(); + //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. virtual void emitAborted(); + //! The Task subclass must call this method when the task has failed virtual void emitFailed(QString reason = ""); virtual void propagateStepProgress(TaskStepProgress const& task_progress); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index d03469b78..75fc93b3b 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -153,7 +153,7 @@ struct TranslationsModel::Private { QDir m_dir; // initial state is just english - QVector m_languages = { Language(defaultLangCode) }; + QList m_languages = { Language(defaultLangCode) }; QString m_selectedLanguage = defaultLangCode; std::unique_ptr m_qt_translator; @@ -417,9 +417,9 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c return 2; } -QVector::Iterator TranslationsModel::findLanguage(const QString& key) +QList::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) @@ -480,7 +480,7 @@ bool TranslationsModel::selectLanguage(QString key) bool successful = false; // FIXME: this is likely never present. FIX IT. d->m_qt_translator.reset(new QTranslator()); - if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) { qCritical() << "Loading Qt Language File failed."; diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 96a0e9f8b..945e689fc 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -41,7 +41,7 @@ class TranslationsModel : public QAbstractListModel { void setUseSystemLocale(bool useSystemLocale); private: - QVector::Iterator findLanguage(const QString& key); + QList::Iterator findLanguage(const QString& key); std::optional findLanguageAsOptional(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); 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 70659741e..664f5ab77 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -123,6 +123,7 @@ #include "KonamiCode.h" #include "InstanceCopyTask.h" +#include "InstanceDirUpdate.h" #include "Json.h" @@ -154,7 +155,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 >:( @@ -175,10 +176,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // restore the instance toolbar settings auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); - if (!APPLICATION->settings()->contains(setting_name)) - instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name); - else - instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); + instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); @@ -308,10 +306,27 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi view->setSelectionMode(QAbstractItemView::SingleSelection); // FIXME: leaks ListViewDelegate - view->setItemDelegate(new ListViewDelegate(this)); + auto delegate = new ListViewDelegate(this); + view->setItemDelegate(delegate); view->setFrameShape(QFrame::NoFrame); // do not show ugly blue border on the mac view->setAttribute(Qt::WA_MacShowFocusRect, false); + connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) { + if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) { + auto oldID = m_selectedInstance->id(); + auto newID = QFileInfo(newRoot).fileName(); + QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID)); + bool syncGroup = origGroup != GroupId() && oldID != newID; + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(oldID, GroupId()); + + refreshInstances(); + setSelectedInstanceById(newID); + + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(newID, origGroup); + } + }); view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); @@ -726,7 +741,7 @@ void MainWindow::changeActiveAccount() QAction* sAction = (QAction*)sender(); // Profile's associated Mojang username - if (sAction->data().type() != QVariant::Type::Int) + if (sAction->data().typeId() != QMetaType::Int) return; QVariant action_data = sAction->data(); @@ -816,11 +831,7 @@ void MainWindow::updateNewsLabel() QList stringToIntList(const QString& string) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList split = string.split(',', Qt::SkipEmptyParts); -#else - QStringList split = string.split(',', QString::SkipEmptyParts); -#endif QList out; for (int i = 0; i < split.size(); ++i) { out.append(split.at(i).toInt()); @@ -1047,6 +1058,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) @@ -1059,19 +1078,19 @@ void MainWindow::processURLs(QList urls) switch (type) { case PackedResourceType::ResourcePack: - minecraftInst->resourcePackList()->installResource(localFileName); + minecraftInst->resourcePackList()->installResourceWithFlameMetadata(localFileName, version); break; case PackedResourceType::TexturePack: - minecraftInst->texturePackList()->installResource(localFileName); + minecraftInst->texturePackList()->installResourceWithFlameMetadata(localFileName, version); break; case PackedResourceType::DataPack: 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); + minecraftInst->shaderPackList()->installResourceWithFlameMetadata(localFileName, version); break; case PackedResourceType::WorldSave: minecraftInst->worldList()->installWorld(localFileInfo); @@ -1312,7 +1331,15 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { - APPLICATION->metacache()->evictAll(); + // This if contains side effects! + if (!APPLICATION->metacache()->evictAll()) { + CustomMessageBox::selectable(this, tr("Error"), + tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " + "Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), + QMessageBox::Warning) + ->show(); + } + APPLICATION->metacache()->SaveNow(); } @@ -1389,20 +1416,8 @@ void MainWindow::on_actionDeleteInstance_triggered() if (response != QMessageBox::Yes) return; - auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); - if (!linkedInstances.empty()) { - response = CustomMessageBox::selectable(this, tr("There are linked instances"), - tr("The following instance(s) might reference files in this instance:\n\n" - "%1\n\n" - "Deleting it could break the other instance(s), \n\n" - "Do you wish to proceed?", - nullptr, linkedInstances.count()) - .arg(linkedInstances.join("\n")), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - if (response != QMessageBox::Yes) - return; - } + if (!checkLinkedInstances(id, this, tr("Deleting"))) + return; if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); @@ -1585,9 +1600,7 @@ void MainWindow::createInstanceShortcut(QString shortcutFilePath) if (DesktopServices::isFlatpak()) { appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); + args.append({ "run", BuildConfig.LAUNCHER_APPID }); } #elif defined(Q_OS_WIN) diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index b652ba991..a8d60aef1 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -77,7 +77,7 @@ QString getCreditsHtml() stream << QString("

d-513 %1

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

txtsd %1

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

timoreo %1

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

Ezekiel Smith (ZekeSmith) %1

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

ZekeZ %1

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

cozyGalvinism %1

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

DioEgizio %1

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

flowln %1

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

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

\n"; stream << QString("

Boba %1

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

Davi Rafael %1

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

AutiOne %1

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

Fulmine %1

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

ely %1

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

gon sawa %1

\n").arg(getGitHub("gonsawa")); diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 5c93053d1..0095f7af9 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -46,11 +46,13 @@ BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, cons : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) { m_hashing_task = shared_qobject_ptr( - new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); @@ -286,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; @@ -293,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; @@ -344,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 @@ -351,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/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/CustomMessageBox.cpp b/launcher/ui/dialogs/CustomMessageBox.cpp index 1af47a449..ca0fe99e0 100644 --- a/launcher/ui/dialogs/CustomMessageBox.cpp +++ b/launcher/ui/dialogs/CustomMessageBox.cpp @@ -21,7 +21,8 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) + QMessageBox::StandardButton defaultButton, + QCheckBox* checkBox) { QMessageBox* messageBox = new QMessageBox(parent); messageBox->setWindowTitle(title); @@ -31,6 +32,8 @@ QMessageBox* selectable(QWidget* parent, messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setIcon(icon); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + if (checkBox) + messageBox->setCheckBox(checkBox); return messageBox; } diff --git a/launcher/ui/dialogs/CustomMessageBox.h b/launcher/ui/dialogs/CustomMessageBox.h index a9bc6a24a..1ee29903e 100644 --- a/launcher/ui/dialogs/CustomMessageBox.h +++ b/launcher/ui/dialogs/CustomMessageBox.h @@ -23,5 +23,6 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon = QMessageBox::NoIcon, QMessageBox::StandardButtons buttons = QMessageBox::Ok, - QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton, + QCheckBox* checkBox = nullptr); } 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 7e00f18f4..51e338503 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -51,6 +51,8 @@ #include #include #include +#include +#include #include #include #include @@ -58,37 +60,40 @@ #include "SeparatorPrefixTree.h" ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) - : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) + : QDialog(parent), m_ui(new Ui::ExportInstanceDialog), m_instance(instance) { - ui->setupUi(this); + m_ui->setupUi(this); auto model = new QFileSystemModel(this); - model->setIconProvider(&icons); + model->setIconProvider(&m_icons); auto root = instance->instanceRoot(); - proxyModel = new FileIgnoreProxy(root, this); - proxyModel->setSourceModel(model); + m_proxyModel = new FileIgnoreProxy(root, this); + m_proxyModel->setSourceModel(model); auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); - proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") }); - proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); - proxyModel->ignoreFilesWithPath().insert( - { FS::PathCombine(prefix, ".cache"), FS::PathCombine(prefix, ".fabric"), FS::PathCombine(prefix, ".quilt") }); - loadPackIgnore(); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { + m_proxyModel->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxyModel->loadBlockedPathsFromFile(ignoreFileName()); - ui->treeView->setModel(proxyModel); - ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); - ui->treeView->sortByColumn(0, Qt::AscendingOrder); + m_ui->treeView->setModel(m_proxyModel); + m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root))); + m_ui->treeView->sortByColumn(0, Qt::AscendingOrder); - connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int))); + connect(m_proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int))); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); - auto headerView = ui->treeView->header(); + auto headerView = m_ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportInstanceDialog::~ExportInstanceDialog() { - delete ui; + delete m_ui; } /// Save icon to instance's folder is needed @@ -139,7 +144,7 @@ void ExportInstanceDialog::doExport() auto files = QFileInfoList(); if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, - std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) { + std::bind(&FileIgnoreProxy::filterFile, m_proxyModel, std::placeholders::_1))) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); QDialog::done(QDialog::Rejected); return; @@ -159,7 +164,7 @@ void ExportInstanceDialog::doExport() void ExportInstanceDialog::done(int result) { - savePackIgnore(); + m_proxyModel->saveBlockedPathsToFile(ignoreFileName()); if (result == QDialog::Accepted) { doExport(); return; @@ -171,13 +176,13 @@ void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) { // WARNING: possible off-by-one? for (int i = top; i < bottom; i++) { - auto node = proxyModel->index(i, 0, parent); - if (proxyModel->shouldExpand(node)) { + auto node = m_proxyModel->index(i, 0, parent); + if (m_proxyModel->shouldExpand(node)) { auto expNode = node.parent(); if (!expNode.isValid()) { continue; } - ui->treeView->expand(node); + m_ui->treeView->expand(node); } } } @@ -186,30 +191,3 @@ QString ExportInstanceDialog::ignoreFileName() { return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } - -void ExportInstanceDialog::loadPackIgnore() -{ - auto filename = ignoreFileName(); - QFile ignoreFile(filename); - if (!ignoreFile.open(QIODevice::ReadOnly)) { - return; - } - auto ignoreData = ignoreFile.readAll(); - auto string = QString::fromUtf8(ignoreData); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); -#else - proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); -#endif -} - -void ExportInstanceDialog::savePackIgnore() -{ - auto ignoreData = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); - auto filename = ignoreFileName(); - try { - FS::write(filename, ignoreData); - } catch (const Exception& e) { - qWarning() << e.cause(); - } -} diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h index 183681f57..989e1635a 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.h +++ b/launcher/ui/dialogs/ExportInstanceDialog.h @@ -60,15 +60,13 @@ class ExportInstanceDialog : public QDialog { private: void doExport(); - void loadPackIgnore(); - void savePackIgnore(); QString ignoreFileName(); private: - Ui::ExportInstanceDialog* ui; + Ui::ExportInstanceDialog* m_ui; InstancePtr m_instance; - FileIgnoreProxy* proxyModel; - FastFileIconProvider icons; + FileIgnoreProxy* m_proxyModel; + FastFileIconProvider m_icons; private slots: void rowsInserted(QModelIndex parent, int top, int bottom); diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 0278c6cb0..303df94a1 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -29,101 +29,102 @@ #include #include #include -#include "FastFileIconProvider.h" #include "FileSystem.h" #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) - : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider) + : QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider) { Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); - ui->setupUi(this); - ui->name->setPlaceholderText(instance->name()); - ui->name->setText(instance->settings()->get("ExportName").toString()); - ui->version->setText(instance->settings()->get("ExportVersion").toString()); - ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + m_ui->setupUi(this); + m_ui->name->setPlaceholderText(instance->name()); + m_ui->name->setText(instance->settings()->get("ExportName").toString()); + m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); + m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { setWindowTitle(tr("Export Modrinth Pack")); - ui->authorLabel->hide(); - ui->author->hide(); + m_ui->authorLabel->hide(); + m_ui->author->hide(); - ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); + m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); } else { setWindowTitle(tr("Export CurseForge Pack")); - ui->summaryLabel->hide(); - ui->summary->hide(); + m_ui->summaryLabel->hide(); + m_ui->summary->hide(); - ui->author->setText(instance->settings()->get("ExportAuthor").toString()); + m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated // the name and version fields mustn't be empty - connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); - connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(m_ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(m_ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); // the instance name can technically be empty validate(); QFileSystemModel* model = new QFileSystemModel(this); - model->setIconProvider(&icons); + model->setIconProvider(&m_icons); // use the game root - everything outside cannot be exported - const QDir root(instance->gameRoot()); - proxy = new FileIgnoreProxy(instance->gameRoot(), this); - proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports", ".cache", ".fabric", ".quilt" }); - proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); - proxy->setSourceModel(model); + const QDir instanceRoot(instance->instanceRoot()); + m_proxy = new FileIgnoreProxy(instance->instanceRoot(), this); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { + m_proxy->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxy->setSourceModel(model); + m_proxy->loadBlockedPathsFromFile(ignoreFileName()); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); - for (const QString& file : root.entryList(filter)) { - if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" || - file == "servers.dat")) - proxy->blockedPaths().insert(file); - } - 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()) + m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); } - ui->files->setModel(proxy); - ui->files->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); - ui->files->sortByColumn(0, Qt::AscendingOrder); + m_ui->files->setModel(m_proxy); + m_ui->files->setRootIndex(m_proxy->mapFromSource(model->index(instance->gameRoot()))); + m_ui->files->sortByColumn(0, Qt::AscendingOrder); model->setFilter(filter); model->setRootPath(instance->gameRoot()); - QHeaderView* headerView = ui->files->header(); + QHeaderView* headerView = m_ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportPackDialog::~ExportPackDialog() { - delete ui; + delete m_ui; } void ExportPackDialog::done(int result) { - auto settings = instance->settings(); - settings->set("ExportName", ui->name->text()); - settings->set("ExportVersion", ui->version->text()); - settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); + m_proxy->saveBlockedPathsToFile(ignoreFileName()); + auto settings = m_instance->settings(); + settings->set("ExportName", m_ui->name->text()); + settings->set("ExportVersion", m_ui->version->text()); + settings->set("ExportOptionalFiles", m_ui->optionalFiles->isChecked()); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - settings->set("ExportSummary", ui->summary->toPlainText()); + settings->set("ExportSummary", m_ui->summary->toPlainText()); else - settings->set("ExportAuthor", ui->author->text()); + settings->set("ExportAuthor", m_ui->author->text()); if (result == Accepted) { - const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); + const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); const QString filename = FS::RemoveInvalidFilenameChars(name); QString output; @@ -145,11 +146,11 @@ void ExportPackDialog::done(int result) Task* task; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { - task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->toPlainText(), ui->optionalFiles->isChecked(), - instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), + m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } else { - task = new FlamePackExportTask(name, ui->version->text(), ui->author->text(), ui->optionalFiles->isChecked(), instance, output, - std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance, + output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } connect(task, &Task::failed, @@ -171,6 +172,11 @@ void ExportPackDialog::done(int result) void ExportPackDialog::validate() { - ui->buttonBox->button(QDialogButtonBox::Ok) - ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && ui->version->text().isEmpty()); + m_ui->buttonBox->button(QDialogButtonBox::Ok) + ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && m_ui->version->text().isEmpty()); +} + +QString ExportPackDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h index 830c24d25..092288d49 100644 --- a/launcher/ui/dialogs/ExportPackDialog.h +++ b/launcher/ui/dialogs/ExportPackDialog.h @@ -41,9 +41,12 @@ class ExportPackDialog : public QDialog { void validate(); private: - const InstancePtr instance; - Ui::ExportPackDialog* ui; - FileIgnoreProxy* proxy; - FastFileIconProvider icons; + QString ignoreFileName(); + + private: + const InstancePtr m_instance; + Ui::ExportPackDialog* m_ui; + FileIgnoreProxy* m_proxy; + FastFileIconProvider m_icons; const ModPlatform::ResourceProvider m_provider; }; diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index 908743ab0..c2ba68f7a 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -64,6 +64,9 @@ ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWi this->ui->finalText->selectAll(); this->ui->finalText->copy(); }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); triggerImp(); } diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui index 3afda2fa8..ec049d7e7 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.ui +++ b/launcher/ui/dialogs/ExportToModListDialog.ui @@ -79,6 +79,9 @@ 0 + + This text supports the following placeholders: {name} - Mod name {mod_id} - Mod ID {url} - Mod URL {version} - Mod version {authors} - Mod authors + diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index a196fd587..8f53995f9 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); @@ -47,7 +58,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - contentsWidget->setItemDelegate(new ListViewDelegate()); + contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget)); // contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); @@ -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::openPath(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 8e5463b31..7082125f2 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -104,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); diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 4e38b4187..83f46294d 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -36,6 +36,7 @@ #include "MSALoginDialog.h" #include "Application.h" +#include "qr.h" #include "ui_MSALoginDialog.h" #include "DesktopServices.h" @@ -60,7 +61,6 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS 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)) { @@ -68,6 +68,8 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS } } }); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); } int MSALoginDialog::exec() @@ -83,7 +85,7 @@ int MSALoginDialog::exec() connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); - m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode, this)); + m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode)); connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject); @@ -137,19 +139,27 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url) m_url = url; } -void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn) +void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { 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)); + if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { + url += QString("?otc=%1").arg(code); } + ui->code->setText(code); + + auto size = QSize(150, 150); + QPixmap pixmap(size); + pixmap.fill(Qt::white); + + QPainter painter(&pixmap); + paintQR(painter, size, url, Qt::black); + + // Set the generated pixmap to the label + ui->qr->setPixmap(pixmap); + + ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString)); } void MSALoginDialog::onDeviceFlowStatus(QString status) diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 375ccc57a..f19abbe6d 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -16,7 +16,6 @@ #pragma once #include -#include #include #include "minecraft/auth/AuthFlow.h" diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index b47b85ff1..d1e420864 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(); } @@ -80,7 +83,8 @@ NewComponentDialog::~NewComponentDialog() void NewComponentDialog::updateDialogState() { auto protoUid = ui->nameTextBox->text().toLower(); - protoUid.remove(QRegularExpression("[^a-z]")); + static const QRegularExpression s_removeChars("[^a-z]"); + protoUid.remove(s_removeChars); if (protoUid.isEmpty()) { ui->uidTextBox->setPlaceholderText(originalPlaceholderText); } else { diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index f16c50860..6036663ba 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -109,16 +109,19 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setDefault(true); OkButton->setAutoDefault(true); + OkButton->setText(tr("OK")); connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); CancelButton->setAutoDefault(false); + CancelButton->setText(tr("Cancel")); connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); auto HelpButton = m_buttons->button(QDialogButtonBox::Help); HelpButton->setDefault(false); HelpButton->setAutoDefault(false); + HelpButton->setText(tr("Help")); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); if (!url.isEmpty()) { @@ -133,11 +136,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, 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)); } diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp index b9d1c2915..d8fbc04fd 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ b/launcher/ui/dialogs/OfflineLoginDialog.cpp @@ -9,6 +9,9 @@ OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(ne ui->progressBar->setVisible(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } diff --git a/launcher/ui/dialogs/OfflineLoginDialog.h b/launcher/ui/dialogs/OfflineLoginDialog.h index a50024a6c..6660a18ec 100644 --- a/launcher/ui/dialogs/OfflineLoginDialog.h +++ b/launcher/ui/dialogs/OfflineLoginDialog.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "minecraft/auth/MinecraftAccount.h" diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index fe03e1b6b..95bdf99a9 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "Application.h" @@ -70,6 +71,9 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSelectDialog::~ProfileSelectDialog() diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 385094e23..0b5e1a784 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -59,9 +59,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg yellowIcon = APPLICATION->getThemedIcon("status-yellow"); badIcon = APPLICATION->getThemedIcon("status-bad"); - QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}"); + static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; - nameEdit->setValidator(new QRegularExpressionValidator(permittedNames)); + nameEdit->setValidator(new QRegularExpressionValidator(s_permittedNames)); nameEdit->setClearButtonEnabled(true); validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); @@ -70,6 +70,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); setNameStatus(NameStatus::NotSet, QString()); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSetupDialog::~ProfileSetupDialog() @@ -223,14 +226,17 @@ struct MojangError { static MojangError fromJSON(QByteArray data) { MojangError out; - out.error = QString::fromUtf8(data); + out.rawError = QString::fromUtf8(data); auto doc = QJsonDocument::fromJson(data, &out.parseError); - auto object = doc.object(); - out.fullyParsed = true; - out.fullyParsed &= Parsers::getString(object.value("path"), out.path); - out.fullyParsed &= Parsers::getString(object.value("error"), out.error); - out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + out.fullyParsed = false; + if (!out.parseError.error) { + auto object = doc.object(); + out.fullyParsed = true; + out.fullyParsed &= Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + } return out; } @@ -258,7 +264,20 @@ void ProfileSetupDialog::setupProfileFinished() } else { 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); + + QString errorMessage = + tr("Network Error: %1\nHTTP Status: %2").arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode())); + + if (parsedError.fullyParsed) { + errorMessage += "Path: " + parsedError.path + "\n"; + errorMessage += "Error: " + parsedError.error + "\n"; + errorMessage += "Message: " + parsedError.errorMessage + "\n"; + } else { + errorMessage += "Failed to parse error from Mojang API: " + parsedError.parseError.errorString() + "\n"; + errorMessage += "Log:\n" + parsedError.rawError + "\n"; + } + + ui->errorLabel->setText(tr("The server responded with the following error:") + "\n\n" + errorMessage); qDebug() << parsedError.rawError; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); button->setEnabled(true); 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 9897687e3..aa2f67bdb 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -92,7 +92,7 @@ ProgressDialog::~ProgressDialog() { for (auto conn : this->m_taskConnections) { disconnect(conn); - } + } delete ui; } diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 4a696a49d..50e4418da 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -93,7 +93,7 @@ class ProgressDialog : public QDialog { Ui::ProgressDialog* ui; Task* m_task; - + QList m_taskConnections; bool m_is_multi_step = false; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 02a8454b4..718c0fe2c 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -18,7 +18,6 @@ */ #include "ResourceDownloadDialog.h" -#include #include #include @@ -148,10 +147,14 @@ void ResourceDownloadDialog::confirm() 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(); } @@ -298,7 +301,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; @@ -377,7 +380,7 @@ QList ShaderPackDownloadDialog::getPages() return pages; } -void ModDownloadDialog::setModMetadata(std::shared_ptr meta) +void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr& meta) { switch (meta->provider) { case ModPlatform::ResourceProvider::MODRINTH: diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 7a0d6e895..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; @@ -107,8 +109,6 @@ class ModDownloadDialog final : public ResourceDownloadDialog { QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; - void setModMetadata(std::shared_ptr); - private: BaseInstance* m_instance; }; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp similarity index 65% rename from launcher/ui/dialogs/ModUpdateDialog.cpp rename to launcher/ui/dialogs/ResourceUpdateDialog.cpp index f906cfcea..aa4bbd294 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -1,4 +1,4 @@ -#include "ModUpdateDialog.h" +#include "ResourceUpdateDialog.h" #include "Application.h" #include "ChooseProviderDialog.h" #include "CustomMessageBox.h" @@ -8,6 +8,7 @@ #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" +#include "tasks/SequentialTask.h" #include "ui_ReviewMessageBox.h" #include "Markdown.h" @@ -36,27 +37,28 @@ static QList mcLoadersList(BaseInstance* inst) 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,28 +89,32 @@ void ModUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loadersList = mcLoadersList(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, loadersList, m_mod_model)); + m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_resource_model)); connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); + [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, loadersList, m_mod_model)); + m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_resource_model)); connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); + [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(); @@ -132,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()); @@ -144,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()); @@ -175,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) { @@ -187,55 +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 dependencyExtraInfo = depTask->getExtraInfo(); - - 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); - auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); - CheckUpdateTask::UpdatableMod updatable = { dep->pack->name, - dep->version.hash, - "", - dep->version.version, - dep->version.version_type, - changelog, - dep->pack->provider, - download_task, - !extraInfo.maybe_installed }; - - appendMod(updatable, extraInfo.required_by); - 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 { @@ -257,35 +266,37 @@ 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) { + // adds resource to list based on provider + 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; } }; + // ask the user on what provider to seach for the mod first for (auto candidate : m_candidates) { - if (candidate->status() != ModStatus::NoMetadata) { + if (candidate->status() != ResourceStatus::NO_METADATA) { onMetadataEnsured(candidate); continue; } @@ -304,7 +315,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())); @@ -326,10 +337,11 @@ auto ModUpdateDialog::ensureMetadata() -> bool addToTmp(candidate, response.chosen); } + // prepare task for the modrinth mods 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, @@ -341,10 +353,11 @@ auto ModUpdateDialog::ensureMetadata() -> bool seq.addTask(modrinth_task); } + // prepare task for the flame mods 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, @@ -358,6 +371,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool seq.addTask(m_second_try_metadata); + // execute all the tasks ProgressDialog checking_dialog(m_parent); checking_dialog.setSkipButton(true, tr("Abort")); checking_dialog.setWindowTitle(tr("Generating metadata...")); @@ -366,18 +380,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; } } @@ -394,31 +408,37 @@ 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); }); + 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](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - - m_second_try_metadata->addTask(task); + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + if (task->getHashingTask()) { + auto seq = makeShared(); + seq->addTask(task->getHashingTask()); + seq->addTask(task); + m_second_try_metadata->addTask(seq); + } else { + m_second_try_metadata->addTask(task); + } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; - 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, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); if (!info.enabled) { - item_top->setToolTip(0, tr("Mod was disabled as it may be already instaled.")); + item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } item_top->setText(0, info.name); item_top->setExpanded(true); @@ -427,7 +447,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri 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)); @@ -462,13 +482,8 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri auto changelog_area = new QTextBrowser(); QString text = info.changelog; - switch (info.provider) { - case ModPlatform::ResourceProvider::MODRINTH: { - text = markdownToHTML(info.changelog.toUtf8()); - break; - } - default: - break; + if (info.provider == ModPlatform::ResourceProvider::MODRINTH) { + text = markdownToHTML(info.changelog.toUtf8()); } changelog_area->setHtml(StringUtils::htmlListPatch(text)); @@ -481,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 66c36d400..96cc8149f 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -20,6 +20,9 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ReviewMessageBox::~ReviewMessageBox() @@ -38,7 +41,7 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); itemTop->setText(0, info.name); if (!info.enabled) { - itemTop->setToolTip(0, tr("Mod was disabled as it may be already instaled.")); + itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } auto filenameItem = new QTreeWidgetItem(itemTop); diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp index c04d87842..1cfb848f4 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.cpp +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -1,4 +1,5 @@ #include "ScrollMessageBox.h" +#include #include "ui_ScrollMessageBox.h" ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) @@ -8,6 +9,9 @@ ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const this->setWindowTitle(title); ui->label->setText(text); ui->textBrowser->setText(body); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ScrollMessageBox::~ScrollMessageBox() diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index 876d7470e..30377288b 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -68,6 +68,9 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, m_buttonBox->setObjectName(QStringLiteral("buttonBox")); m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_horizontalLayout->addWidget(m_buttonBox); m_verticalLayout->addLayout(m_horizontalLayout); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 65840eb08..3bc0bc2d9 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.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 @@ -17,6 +17,7 @@ */ #include "SkinManageDialog.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" #include "ui_SkinManageDialog.h" #include @@ -52,13 +53,15 @@ #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) + : QDialog(parent), m_acct(acct), m_ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) { - ui->setupUi(this); + m_ui->setupUi(this); + + m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); setWindowModality(Qt::WindowModal); - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); @@ -88,25 +91,31 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); - connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); setupCapes(); - ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); } SkinManageDialog::~SkinManageDialog() { - delete ui; + delete m_ui; + delete m_skinPreview; } void SkinManageDialog::activated(QModelIndex index) { - m_selected_skin = index.data(Qt::UserRole).toString(); + m_selectedSkinKey = index.data(Qt::UserRole).toString(); accept(); } -void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected) { if (selected.empty()) return; @@ -114,19 +123,20 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection 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()) + m_selectedSkinKey = key; + auto skin = getSelectedSkin(); + if (!skin) 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); + + m_skinPreview->updateScene(skin); + m_ui->capeCombo->setCurrentIndex(m_capesIdx.value(skin->getCapeId())); + m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); + m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); } void SkinManageDialog::delayed_scroll(QModelIndex model_index) { - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->scrollTo(model_index); } @@ -149,23 +159,19 @@ void SkinManageDialog::on_fileBtn_clicked() } } -QPixmap previewCape(QPixmap capeImage) +QPixmap previewCape(QImage 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); + return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).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()); + m_ui->capeCombo->addItem(tr("No Cape"), QVariant()); auto currentCape = accountData.minecraftProfile.currentCape; if (currentCape.isEmpty()) { - ui->capeCombo->setCurrentIndex(index); + m_ui->capeCombo->setCurrentIndex(index); } auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); @@ -174,9 +180,9 @@ void SkinManageDialog::setupCapes() for (auto& cape : accountData.minecraftProfile.capes) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (cape.data.size()) { - QPixmap capeImage; + QImage capeImage; if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { - m_capes[cape.id] = previewCape(capeImage); + m_capes[cape.id] = capeImage; continue; } } @@ -194,46 +200,50 @@ void SkinManageDialog::setupCapes() } for (auto& cape : accountData.minecraftProfile.capes) { index++; - QPixmap capeImage; + QImage 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); + m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id); } else { - ui->capeCombo->addItem(cape.alias, cape.id); + m_ui->capeCombo->addItem(cape.alias, cape.id); } - m_capes_idx[cape.id] = index; + m_capesIdx[cape.id] = index; } } void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { - auto id = ui->capeCombo->currentData(); + auto id = m_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)); + m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + } else { + m_ui->capeImage->clear(); } - if (auto skin = m_list.skin(m_selected_skin); skin) { + m_skinPreview->updateCape(cape); + if (auto skin = getSelectedSkin(); skin) { skin->setCapeId(id.toString()); + m_skinPreview->updateScene(skin); } } void SkinManageDialog::on_steveBtn_toggled(bool checked) { - if (auto skin = m_list.skin(m_selected_skin); skin) { + if (auto skin = getSelectedSkin(); skin) { skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + m_skinPreview->updateScene(skin); } } void SkinManageDialog::accept() { - auto skin = m_list.skin(m_selected_skin); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) { reject(); return; @@ -283,15 +293,15 @@ void SkinManageDialog::on_resetBtn_clicked() 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.addAction(m_ui->action_Rename_Skin); + myMenu.addAction(m_ui->action_Delete_Skin); - myMenu.exec(ui->listView->mapToGlobal(pos)); + myMenu.exec(m_ui->listView->mapToGlobal(pos)); } bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) { - if (obj == ui->listView) { + if (obj == m_ui->listView) { if (ev->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(ev); switch (keyEvent->key()) { @@ -311,22 +321,22 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) { - if (!m_selected_skin.isEmpty()) { - ui->listView->edit(ui->listView->currentIndex()); + if (!m_selectedSkinKey.isEmpty()) { + m_ui->listView->edit(m_ui->listView->currentIndex()); } } void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) { - if (m_selected_skin.isEmpty()) + if (m_selectedSkinKey.isEmpty()) return; - if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) { + if (m_list.getSkinIndex(m_selectedSkinKey) == 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); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) return; @@ -338,15 +348,15 @@ void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) ->exec(); if (response == QMessageBox::Yes) { - if (!m_list.deleteSkin(m_selected_skin, true)) { - m_list.deleteSkin(m_selected_skin, false); + if (!m_list.deleteSkin(m_selectedSkinKey, true)) { + m_list.deleteSkin(m_selectedSkinKey, false); } } } void SkinManageDialog::on_urlBtn_clicked() { - auto url = QUrl(ui->urlLine->text()); + auto url = QUrl(m_ui->urlLine->text()); if (!url.isValid()) { CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); return; @@ -363,13 +373,13 @@ void SkinManageDialog::on_urlBtn_clicked() 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()), + : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()), QMessageBox::Critical) ->show(); QFile::remove(path); return; } - ui->urlLine->setText(""); + m_ui->urlLine->setText(""); if (QFileInfo(path).suffix().isEmpty()) { QFile::rename(path, path + ".png"); } @@ -402,7 +412,7 @@ class WaitTask : public Task { void SkinManageDialog::on_userBtn_clicked() { - auto user = ui->urlLine->text(); + auto user = m_ui->urlLine->text(); if (user.isEmpty()) { return; } @@ -496,7 +506,7 @@ void SkinManageDialog::on_userBtn_clicked() QFile::remove(path); return; } - ui->urlLine->setText(""); + m_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)) { @@ -510,14 +520,24 @@ 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 id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } else { + m_ui->capeImage->clear(); } } + +SkinModel* SkinManageDialog::getSelectedSkin() +{ + if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid()) { + return skin; + } + return nullptr; +} + +QHash SkinManageDialog::capes() +{ + return m_capes; +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h index cdb37a513..c6a6c9fcd 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.h +++ b/launcher/ui/dialogs/skins/SkinManageDialog.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 @@ -24,18 +24,22 @@ #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" namespace Ui { class SkinManageDialog; } - -class SkinManageDialog : public QDialog { +class SkinManageDialog : public QDialog, public SkinProvider { Q_OBJECT public: explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); virtual ~SkinManageDialog(); void resizeEvent(QResizeEvent* event) override; + virtual SkinModel* getSelectedSkin() override; + virtual QHash capes() override; + public slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); @@ -56,10 +60,12 @@ class SkinManageDialog : public QDialog { private: void setupCapes(); + private: MinecraftAccountPtr m_acct; - Ui::SkinManageDialog* ui; + Ui::SkinManageDialog* m_ui; SkinList m_list; - QString m_selected_skin; - QHash m_capes; - QHash m_capes_idx; + QString m_selectedSkinKey; + QHash m_capes; + QHash m_capesIdx; + SkinOpenGLWindow* m_skinPreview = nullptr; }; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index c77eeaaa3..7e8b4bc46 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -19,17 +19,7 @@ - - - - - - false - - - Qt::AlignCenter - - + diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp new file mode 100644 index 000000000..b4ab8d4cc --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.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 . + */ + +#include "BoxGeometry.h" + +#include +#include +#include +#include + +struct VertexData { + QVector4D position; + QVector2D texCoord; + VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex) {} +}; + +// For cube we would need only 8 vertices but we have to +// duplicate vertex for each face because texture coordinate +// is different. +static const QList vertices = { + // Vertex data for face 0 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3 + // Vertex data for face 1 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7 + + // Vertex data for face 2 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11 + + // Vertex data for face 3 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15 + + // Vertex data for face 4 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19 + + // Vertex data for face 5 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23 +}; + +// Indices for drawing cube faces using triangle strips. +// Triangle strips can be connected by duplicating indices +// between the strips. If connecting strips have opposite +// vertex order then last index of the first strip and first +// index of the second strip needs to be duplicated. If +// connecting strips have same vertex order then only last +// index of the first strip needs to be duplicated. +static const QList indices = { + 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) + 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) + 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) + 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) + 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) + 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) +}; + +static const QList planeVertices = { + { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left + { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right + { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left + { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right +}; +static const QList planeIndices = { + 0, 1, 2, 3, 3 // Face 0 - triangle strip ( v0, v1, v2, v3) +}; + +QList transformVectors(const QMatrix4x4& matrix, const QList& vectors) +{ + QList transformedVectors; + transformedVectors.reserve(vectors.size()); + + for (const QVector4D& vec : vectors) { + if (!matrix.isIdentity()) { + transformedVectors.append(matrix * vec); + } else { + transformedVectors.append(vec); + } + } + + return transformedVectors; +} + +// Function to calculate UV coordinates +// this is pure magic (if something is wrong with textures this is at fault) +QList getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList { + return { + QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight), + QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight), + }; + }; + + auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth); + auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); + auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height); + auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); + auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); + auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); + + auto uvRight = { + right[0], + right[1], + right[3], + right[2], + }; + auto uvLeft = { + left[0], + left[1], + left[3], + left[2], + }; + auto uvTop = { + top[0], + top[1], + top[3], + top[2], + }; + auto uvBottom = { + bottom[3], + bottom[2], + bottom[0], + bottom[1], + }; + auto uvFront = { + front[0], + front[1], + front[3], + front[2], + }; + auto uvBack = { + back[0], + back[1], + back[3], + back[2], + }; + // Create a new array to hold the modified UV data + QList uvData; + uvData.reserve(24); + + // Iterate over the arrays and copy the data to newUVData + for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop }) { + uvData.append(uvArray); + } + + return uvData; +} + +namespace opengl { +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) +{ + initializeOpenGLFunctions(); + + // Generate 2 VBOs + m_vertexBuf.create(); + m_indexBuf.create(); +} + +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize) + : BoxGeometry(size, position) +{ + initGeometry(uv.x(), uv.y(), textureDim.x(), textureDim.y(), textureDim.z(), textureSize.width(), textureSize.height()); +} + +BoxGeometry::~BoxGeometry() +{ + m_vertexBuf.destroy(); + m_indexBuf.destroy(); +} + +void BoxGeometry::draw(QOpenGLShaderProgram* program) +{ + // Tell OpenGL which VBOs to use + program->setUniformValue("model_matrix", m_matrix); + m_vertexBuf.bind(); + m_indexBuf.bind(); + + // Offset for position + quintptr offset = 0; + + // Tell OpenGL programmable pipeline how to locate vertex position data + int vertexLocation = program->attributeLocation("a_position"); + program->enableAttributeArray(vertexLocation); + program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData)); + + // Offset for texture coordinate + offset += sizeof(QVector4D); + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + int texcoordLocation = program->attributeLocation("a_texcoord"); + program->enableAttributeArray(texcoordLocation); + program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr); +} + +void BoxGeometry::initGeometry(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight); + + // this should not be needed to be done on each render for most of the objects + QMatrix4x4 transformation; + transformation.translate(m_position); + transformation.scale(m_size); + auto positions = transformVectors(transformation, vertices); + + QList verticesData; + verticesData.reserve(positions.size()); // Reserve space for efficiency + + for (int i = 0; i < positions.size(); ++i) { + verticesData.append(VertexData(positions[i], textureCord[i])); + } + + // Transfer vertex data to VBO 0 + m_vertexBuf.bind(); + m_vertexBuf.allocate(verticesData.constData(), verticesData.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + m_indexBuf.bind(); + m_indexBuf.allocate(indices.constData(), indices.size() * sizeof(GLushort)); + m_indecesCount = indices.size(); +} + +void BoxGeometry::rotate(float angle, const QVector3D& vector) +{ + m_matrix.rotate(angle, vector); +} + +BoxGeometry* BoxGeometry::Plane() +{ + auto b = new BoxGeometry(QVector3D(), QVector3D()); + + // Transfer vertex data to VBO 0 + b->m_vertexBuf.bind(); + b->m_vertexBuf.allocate(planeVertices.constData(), planeVertices.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + b->m_indexBuf.bind(); + b->m_indexBuf.allocate(planeIndices.constData(), planeIndices.size() * sizeof(GLushort)); + b->m_indecesCount = planeIndices.size(); + + return b; +} +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/launcher/ui/dialogs/skins/draw/BoxGeometry.h new file mode 100644 index 000000000..1a245bc14 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.h @@ -0,0 +1,48 @@ +// 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 +#include +#include +#include + +namespace opengl { +class BoxGeometry : protected QOpenGLFunctions { + public: + BoxGeometry(QVector3D size, QVector3D position); + BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize = { 64, 64 }); + static BoxGeometry* Plane(); + virtual ~BoxGeometry(); + + void draw(QOpenGLShaderProgram* program); + + void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); + void rotate(float angle, const QVector3D& vector); + + private: + QOpenGLBuffer m_vertexBuf; + QOpenGLBuffer m_indexBuf; + QVector3D m_size; + QVector3D m_position; + QMatrix4x4 m_matrix; + GLsizei m_indecesCount; +}; +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp new file mode 100644 index 000000000..45d0ba191 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -0,0 +1,134 @@ + +// 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 "ui/dialogs/skins/draw/Scene.h" +namespace opengl { +Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_capeVisible(!cape.isNull()) +{ + m_staticComponents = { + // head + new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), + new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), + // body + new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), + new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), + // right leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9, -18, -0.1), QPoint(0, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), + // left leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(1.9, -18, -0.1), QPoint(0, 48), QVector3D(4, 12, 4)), + }; + m_normalArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(6, -6, 0), QPoint(48, 48), QVector3D(4, 12, 4)), + }; + + m_slimArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(5.5, -6, 0), QPoint(48, 48), QVector3D(3, 12, 4)), + }; + + m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), QVector3D(0, -8, 2.5), QPoint(0, 0), QVector3D(10, 16, 1), QSize(64, 32)); + m_cape->rotate(10.8, QVector3D(1, 0, 0)); + m_cape->rotate(180, QVector3D(0, 1, 0)); + + // texture init + m_skinTexture = new QOpenGLTexture(skin.mirrored()); + m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest); + + m_capeTexture = new QOpenGLTexture(cape.mirrored()); + m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} +Scene::~Scene() +{ + for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) { + for (auto g : array) { + delete g; + } + } + delete m_cape; + + m_skinTexture->destroy(); + delete m_skinTexture; + + m_capeTexture->destroy(); + delete m_capeTexture; +} + +void Scene::draw(QOpenGLShaderProgram* program) +{ + m_skinTexture->bind(); + program->setUniformValue("texture", 0); + for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms }) { + for (auto g : toDraw) { + g->draw(program); + } + } + m_skinTexture->release(); + if (m_capeVisible) { + m_capeTexture->bind(); + program->setUniformValue("texture", 0); + m_cape->draw(program); + m_capeTexture->release(); + } +} + +void updateTexture(QOpenGLTexture* texture, const QImage& img) +{ + if (texture) { + if (texture->isBound()) + texture->release(); + texture->destroy(); + texture->create(); + texture->setSize(img.width(), img.height()); + texture->setData(img); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + } +} + +void Scene::setSkin(const QImage& skin) +{ + updateTexture(m_skinTexture, skin.mirrored()); +} + +void Scene::setMode(bool slim) +{ + m_slim = slim; +} +void Scene::setCape(const QImage& cape) +{ + updateTexture(m_capeTexture, cape.mirrored()); +} +void Scene::setCapeVisible(bool visible) +{ + m_capeVisible = visible; +} +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h new file mode 100644 index 000000000..3560d1d74 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -0,0 +1,46 @@ +// 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 "ui/dialogs/skins/draw/BoxGeometry.h" + +#include +namespace opengl { +class Scene { + public: + Scene(const QImage& skin, bool slim, const QImage& cape); + virtual ~Scene(); + + void draw(QOpenGLShaderProgram* program); + void setSkin(const QImage& skin); + void setCape(const QImage& cape); + void setMode(bool slim); + void setCapeVisible(bool visible); + + private: + QList m_staticComponents; + QList m_normalArms; + QList m_slimArms; + BoxGeometry* m_cape = nullptr; + QOpenGLTexture* m_skinTexture = nullptr; + QOpenGLTexture* m_capeTexture = nullptr; + bool m_slim = false; + bool m_capeVisible = false; +}; +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp new file mode 100644 index 000000000..e1e539050 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -0,0 +1,265 @@ +// 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 "ui/dialogs/skins/draw/SkinOpenGLWindow.h" + +#include +#include +#include +#include +#include + +#include "minecraft/skins/SkinModel.h" +#include "rainbow.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color) + : QOpenGLWindow(), QOpenGLFunctions(), m_baseColor(color), m_parent(parent) +{ + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setDepthBufferSize(24); + setFormat(format); +} + +SkinOpenGLWindow::~SkinOpenGLWindow() +{ + // Make sure the context is current when deleting the texture + // and the buffers. + makeCurrent(); + // double check if resources were initialized because they are not + // initialized together with the object + if (m_scene) { + delete m_scene; + } + if (m_background) { + delete m_background; + } + if (m_backgroundTexture) { + if (m_backgroundTexture->isCreated()) { + m_backgroundTexture->destroy(); + } + delete m_backgroundTexture; + } + if (m_program) { + if (m_program->isLinked()) { + m_program->release(); + } + m_program->removeAllShaders(); + delete m_program; + } + doneCurrent(); +} + +void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) +{ + // Save mouse press position + m_mousePosition = QVector2D(e->pos()); + m_isMousePressed = true; +} + +void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) +{ + if (m_isMousePressed) { + int dx = event->position().x() - m_mousePosition.x(); + int dy = event->position().y() - m_mousePosition.y(); + + m_yaw += dx * 0.5f; + m_pitch += dy * 0.5f; + + // Normalize yaw to keep it manageable + if (m_yaw > 360.0f) + m_yaw -= 360.0f; + else if (m_yaw < 0.0f) + m_yaw += 360.0f; + + m_mousePosition = QVector2D(event->pos()); + update(); // Trigger a repaint + } +} + +void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e) +{ + m_isMousePressed = false; +} + +void SkinOpenGLWindow::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0, 0, 1, 1); + + initShaders(); + + generateBackgroundTexture(32, 32, 1); + + QImage skin, cape; + bool slim = false; + if (m_parent) { + if (auto s = m_parent->getSelectedSkin()) { + skin = s->getTexture(); + slim = s->getModel() == SkinModel::SLIM; + cape = m_parent->capes().value(s->getCapeId(), {}); + } + } + + m_scene = new opengl::Scene(skin, slim, cape); + m_background = opengl::BoxGeometry::Plane(); + glEnable(GL_TEXTURE_2D); +} + +void SkinOpenGLWindow::initShaders() +{ + m_program = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader.glsl")) + close(); + + // Compile fragment shader + if (!m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_program->link()) + close(); + + // Bind shader pipeline for use + if (!m_program->bind()) + close(); +} + +void SkinOpenGLWindow::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + const qreal zNear = .1, zFar = 1000., fov = 45; + + // Reset projection + m_projection.setToIdentity(); + + // Set perspective projection + m_projection.perspective(fov, aspect, zNear, zFar); +} + +void SkinOpenGLWindow::paintGL() +{ + // Clear color and depth buffer + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth buffer + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Enable back face culling + glEnable(GL_CULL_FACE); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_program->bind(); + + renderBackground(); + // Calculate model view transformation + QMatrix4x4 matrix; + float yawRad = qDegreesToRadians(m_yaw); + float pitchRad = qDegreesToRadians(m_pitch); + matrix.lookAt(QVector3D( // + m_distance * qCos(pitchRad) * qCos(yawRad), // + m_distance * qSin(pitchRad) - 8, // + m_distance * qCos(pitchRad) * qSin(yawRad)), + QVector3D(0, -8, 0), QVector3D(0, 1, 0)); + + // Set modelview-projection matrix + m_program->setUniformValue("mvp_matrix", m_projection * matrix); + + m_scene->draw(m_program); + m_program->release(); +} + +void SkinOpenGLWindow::updateScene(SkinModel* skin) +{ + if (skin && m_scene) { + m_scene->setMode(skin->getModel() == SkinModel::SLIM); + m_scene->setSkin(skin->getTexture()); + update(); + } +} +void SkinOpenGLWindow::updateCape(const QImage& cape) +{ + if (m_scene) { + m_scene->setCapeVisible(!cape.isNull()); + m_scene->setCape(cape); + update(); + } +} + +QColor calculateContrastingColor(const QColor& color) +{ + constexpr float contrast = 0.2; + auto luma = Rainbow::luma(color); + if (luma < 0.5) { + return Rainbow::lighten(color, contrast); + } else { + return Rainbow::darken(color, contrast); + } +} + +QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) +{ + QImage image(width, height, QImage::Format_RGB888); + auto white = baseColor; + auto black = calculateContrastingColor(baseColor); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + bool isWhite = ((x / tileSize) % 2) == ((y / tileSize) % 2); + image.setPixelColor(x, y, isWhite ? white : black); + } + } + return image; +} + +void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize) +{ + m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor)); + m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} + +void SkinOpenGLWindow::renderBackground() +{ + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Disable depth buffer writing + m_backgroundTexture->bind(); + QMatrix4x4 matrix; + m_program->setUniformValue("mvp_matrix", matrix); + m_program->setUniformValue("texture", 0); + m_background->draw(m_program); + m_backgroundTexture->release(); + glDepthMask(GL_TRUE); // Re-enable depth buffer writing + glEnable(GL_DEPTH_TEST); +} + +void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) +{ + // Adjust distance based on scroll + int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down + m_distance -= delta * 0.01f; // Adjust sensitivity factor + m_distance = qMax(16.f, m_distance); // Clamp distance + update(); // Trigger a repaint +} diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h new file mode 100644 index 000000000..e2c32da0f --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -0,0 +1,79 @@ +// 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 +#include +#include +#include +#include +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +class SkinProvider { + public: + virtual ~SkinProvider() = default; + virtual SkinModel* getSelectedSkin() = 0; + virtual QHash capes() = 0; +}; +class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { + Q_OBJECT + + public: + SkinOpenGLWindow(SkinProvider* parent, QColor color); + virtual ~SkinOpenGLWindow(); + + void updateScene(SkinModel* skin); + void updateCape(const QImage& cape); + + protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void initShaders(); + + void generateBackgroundTexture(int width, int height, int tileSize); + void renderBackground(); + + private: + QOpenGLShaderProgram* m_program; + opengl::Scene* m_scene = nullptr; + + QMatrix4x4 m_projection; + + QVector2D m_mousePosition; + + bool m_isMousePressed = false; + float m_distance = 48; + float m_yaw = 90; // Horizontal rotation angle + float m_pitch = 0; // Vertical rotation angle + + opengl::BoxGeometry* m_background = nullptr; + QOpenGLTexture* m_backgroundTexture = nullptr; + QColor m_baseColor; + SkinProvider* m_parent = nullptr; +}; diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index d947163bc..c7115801e 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -397,6 +397,7 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, // Prevent instance names longer than 128 chars text.truncate(128); if (text.size() != 0) { + emit textChanged(model->data(index).toString(), text); model->setData(index, text); } } diff --git a/launcher/ui/instanceview/InstanceDelegate.h b/launcher/ui/instanceview/InstanceDelegate.h index 69dd32ba7..98ff9a2fc 100644 --- a/launcher/ui/instanceview/InstanceDelegate.h +++ b/launcher/ui/instanceview/InstanceDelegate.h @@ -33,6 +33,9 @@ class ListViewDelegate : public QStyledItemDelegate { void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + signals: + void textChanged(QString before, QString after) const; + private slots: void editingDone(); }; diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index c677f3951..2349c684d 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -89,7 +89,7 @@ void InstanceView::setModel(QAbstractItemModel* model) void InstanceView::dataChanged([[maybe_unused]] const QModelIndex& topLeft, [[maybe_unused]] const QModelIndex& bottomRight, - [[maybe_unused]] const QVector& roles) + [[maybe_unused]] const QList& roles) { scheduleDelayedItemsLayout(); } @@ -400,12 +400,8 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event) if (event->button() == Qt::LeftButton) { emit clicked(index); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if (m_pressedAlreadySelected) { option.state |= QStyle::State_Selected; } @@ -422,7 +418,7 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QModelIndex index = indexAt(event->pos()); if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) { - QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(), event->screenPos(), event->button(), + QMouseEvent me(QEvent::MouseButtonPress, event->position(), event->scenePosition(), event->globalPosition(), event->button(), event->buttons(), event->modifiers()); mousePressEvent(&me); return; @@ -431,12 +427,8 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QPersistentModelIndex persistent = index; emit doubleClicked(persistent); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); } @@ -472,12 +464,8 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) painter.setOpacity(1.0); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.widget = this; if (model()->rowCount() == 0) { @@ -610,7 +598,7 @@ void InstanceView::dragEnterEvent(QDragEnterEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -622,7 +610,7 @@ void InstanceView::dragMoveEvent(QDragMoveEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -648,7 +636,7 @@ void InstanceView::dropEvent(QDropEvent* event) if (event->source() == this) { if (event->possibleActions() & Qt::MoveAction) { - std::pair dropPos = rowDropPos(event->pos()); + std::pair dropPos = rowDropPos(event->position().toPoint()); const VisualGroup* group = dropPos.first; auto hitResult = dropPos.second; @@ -732,12 +720,8 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const int x = pos.first; // int y = pos.second; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif QRect out; out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); @@ -784,12 +768,8 @@ QPixmap InstanceView::renderToPixmap(const QModelIndexList& indices, QRect* r) c QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 30be411a8..dea8b1212 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -83,7 +83,7 @@ class InstanceView : public QAbstractItemView { virtual void updateGeometries() override; protected slots: - virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) override; + virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override; virtual void rowsInserted(const QModelIndex& parent, int start, int end) override; virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void modelReset(); diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 83103c502..4f7a61eb5 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -55,7 +55,7 @@ void VisualGroup::update() auto itemsPerRow = view->itemsPerRow(); int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow)); - rows = QVector(numRows); + rows = QList(numRows); int maxRowHeight = 0; int positionInRow = 0; @@ -73,12 +73,8 @@ void VisualGroup::update() positionInRow = 0; maxRowHeight = 0; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem viewItemOption; view->initViewItemOption(&viewItemOption); -#else - QStyleOptionViewItem viewItemOption = view->viewOptions(); -#endif auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); if (itemHeight > maxRowHeight) { diff --git a/launcher/ui/instanceview/VisualGroup.h b/launcher/ui/instanceview/VisualGroup.h index 8c6f06bcc..7210e0dfc 100644 --- a/launcher/ui/instanceview/VisualGroup.h +++ b/launcher/ui/instanceview/VisualGroup.h @@ -35,10 +35,10 @@ #pragma once +#include #include #include #include -#include class InstanceView; class QPainter; @@ -61,7 +61,7 @@ struct VisualGroup { InstanceView* view = nullptr; QString text; bool collapsed = false; - QVector rows; + QList rows; int firstItemIndex = 0; int m_verticalPosition = 0; diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 0ece3220b..5f69b9d46 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -132,9 +132,9 @@ class InstallJavaPage : public QWidget, public BasePage { m_recommended_majors = majors; recommendedFilterChanged(); } - void setRecomend(bool recomend) + void setRecommend(bool recommend) { - m_recommend = recomend; + m_recommend = recommend; recommendedFilterChanged(); } void recommendedFilterChanged() @@ -202,7 +202,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget recommendedCheckBox->setCheckState(Qt::CheckState::Checked); connect(recommendedCheckBox, &QCheckBox::stateChanged, this, [this](int state) { for (BasePage* page : container->getPages()) { - pageCast(page)->setRecomend(state == Qt::Checked); + pageCast(page)->setRecommend(state == Qt::Checked); } }); @@ -212,6 +212,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttons->button(QDialogButtonBox::Ok)->setText(tr("Download")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); @@ -260,7 +261,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget container->selectPage(page->id()); auto cast = pageCast(page); - cast->setRecomend(true); + cast->setRecommend(true); connect(cast, &InstallJavaPage::selectionChanged, this, [this, cast] { validate(cast); }); if (!recommendedJavas.isEmpty()) { cast->setRecommendedMajors(recommendedJavas); @@ -316,7 +317,7 @@ void InstallDialog::done(int result) deletePath(); } #if defined(Q_OS_MACOS) - auto seq = makeShared(this, tr("Install Java")); + auto seq = makeShared(tr("Install Java")); seq->addTask(task); seq->addTask(makeShared(final_path)); task = seq; @@ -343,4 +344,4 @@ void InstallDialog::done(int result) } // namespace Java -#include "InstallJavaDialog.moc" \ No newline at end of file +#include "InstallJavaDialog.moc" diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 6514217cd..d211cb4d3 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -39,6 +39,8 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); buttons->button(QDialogButtonBox::Close)->setDefault(true); + buttons->button(QDialogButtonBox::Close)->setText(tr("Close")); + buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index a137c4cde..a030bf316 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -59,10 +59,10 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, PasteUpload::PasteType::Hastebin }; - static QRegularExpression validUrlRegExp("https?://.+"); - static QRegularExpression validMSAClientID( + static const QRegularExpression s_validUrlRegExp("https?://.+"); + static const QRegularExpression s_validMSAClientID( QRegularExpression::anchoredPattern("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); - static QRegularExpression validFlameKey(QRegularExpression::anchoredPattern("\\$2[ayb]\\$.{56}")); + static const QRegularExpression s_validFlameKey(QRegularExpression::anchoredPattern("\\$2[ayb]\\$.{56}")); ui->setupUi(this); @@ -75,10 +75,10 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); // NOTE: this allows http://, but we replace that with https later anyway - ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL)); - ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); - ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); - ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); + ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); + ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); + ui->flameKey->setValidator(new QRegularExpressionValidator(s_validFlameKey, ui->flameKey)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 9c713aa79..05c256bb2 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -207,7 +207,7 @@ - <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/#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 diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 4f02b7df5..7bd5101c0 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: diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp deleted file mode 100644 index cc8518c2f..000000000 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * 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 "CustomCommandsPage.h" -#include -#include -#include - -CustomCommandsPage::CustomCommandsPage(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")); - commands = new CustomCommands(this); - commands->setContentsMargins(6, 6, 6, 6); - tabWidget->addTab(commands, "Foo"); - tabWidget->tabBar()->hide(); - verticalLayout->addWidget(tabWidget); - loadSettings(); -} - -CustomCommandsPage::~CustomCommandsPage() {} - -bool CustomCommandsPage::apply() -{ - applySettings(); - return true; -} - -void CustomCommandsPage::applySettings() -{ - 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(); -} 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/EnvironmentVariablesPage.h b/launcher/ui/pages/global/EnvironmentVariablesPage.h deleted file mode 100644 index 6e80775ec..000000000 --- a/launcher/ui/pages/global/EnvironmentVariablesPage.h +++ /dev/null @@ -1,42 +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 . - */ - -#pragma once - -#include - -#include "ui/pages/BasePage.h" -#include "ui/widgets/EnvironmentVariables.h" - -class EnvironmentVariablesPage : public QWidget, public BasePage { - Q_OBJECT - - public: - explicit EnvironmentVariablesPage(QWidget* parent = nullptr); - - QString displayName() const override; - QIcon icon() const override; - QString id() const override; - QString helpPage() const override; - - bool apply() override; - void retranslate() override; - - private: - EnvironmentVariables* variables; -}; diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 0ae296815..6a44c9290 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -69,18 +69,8 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) ui->managedJavaList->selectCurrent(); ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed")); ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!")); - connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { - ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); - if (!ui->autodetectJavaCheckBox->isChecked()) - ui->autodownloadCheckBox->setChecked(false); - }); - } else { - ui->autodownloadCheckBox->setHidden(true); + } else ui->tabWidget->tabBar()->hide(); - } - - loadSettings(); - updateThresholds(); } JavaPage::~JavaPage() @@ -88,117 +78,18 @@ JavaPage::~JavaPage() delete ui; } +void JavaPage::retranslate() +{ + ui->retranslateUi(this); +} + bool JavaPage::apply() { - applySettings(); + ui->javaSettings->saveSettings(); + JavaCommon::checkJVMArgs(APPLICATION->settings()->get("JvmArgs").toString(), this); 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()); - s->set("AutomaticJavaSwitch", ui->autodetectJavaCheckBox->isChecked()); - s->set("AutomaticJavaDownload", ui->autodownloadCheckBox->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()); - ui->autodetectJavaCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool()); - ui->autodownloadCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool() && s->get("AutomaticJavaDownload").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); - if (!java->is_64bit && APPLICATION->settings()->get("MaxMemAlloc").toInt() > 2048) { - CustomMessageBox::selectable(this, tr("Confirm Selection"), - tr("You selected a 32-bit version of Java.\n" - "This installation does not support more than 2048MiB of RAM.\n" - "Please make sure that the maximum memory value is lower."), - QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) - ->exec(); - } - } -} - -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_downloadJavaButton_clicked() { auto jdialog = new Java::InstallDialog({}, nullptr, this); @@ -206,51 +97,6 @@ void JavaPage::on_downloadJavaButton_clicked() ui->managedJavaList->loadList(); } -void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) -{ - updateThresholds(); -} - -void JavaPage::checkerFinished() -{ - checker.reset(); -} - -void JavaPage::retranslate() -{ - ui->retranslateUi(this); -} - -void JavaPage::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); - } -} - void JavaPage::on_removeJavaButton_clicked() { auto version = ui->managedJavaList->selectedVersion(); diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 0a1c4a6be..b30fa22e3 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -41,6 +41,7 @@ #include #include "JavaCommon.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/JavaSettingsWidget.h" class SettingsObject; @@ -59,26 +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_downloadJavaButton_clicked(); void on_removeJavaButton_clicked(); void on_refreshJavaButton_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - void checkerFinished(); 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 e6bbeb15a..a4b2ac203 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -40,292 +40,27 @@ - - - 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 + + + + + + + + - - - - Java Runtime - - - - - - - 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... - - - - - - - - 0 - 0 - - - - &Test - - - - - - - - - - 0 - 0 - - - - JVM arguments: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - - - Skip Java &Wizard - - - - - - - true - - - - 0 - 0 - - - - - 16777215 - 100 - - - - - - - - Automatically selects the Java version that is compatible with the current Minecraft instance, based on the major version required. - - - Autodetect Java version - - - - - - - - - - 0 - 0 - - - - &Java path: - - - javaPathTextBox - - - - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - false - - - Automatically downloads and selects the Java version recommended by Mojang. - - - Auto-download Mojang Java - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -416,20 +151,13 @@
ui/widgets/VersionSelectWidget.h
1 + + JavaSettingsWidget + QWidget +
ui/widgets/JavaSettingsWidget.h
+ 1 +
- - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaPathTextBox - javaBrowseBtn - javaDetectBtn - javaTestBtn - skipCompatibilityCheckbox - skipJavaWizardCheckbox - jvmArgsTextBox - tabWidget - diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 8bbed9643..d89a65a85 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -65,6 +65,15 @@ enum InstSortMode { Sort_LastLaunch }; +enum InstRenamingMode { + // Rename metadata only. + Rename_Always, + // Ask everytime. + Rename_Ask, + // Rename physical directory too. + Rename_Never +}; + LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); @@ -217,9 +226,6 @@ void LauncherPage::applySettings() 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()); @@ -235,7 +241,9 @@ void LauncherPage::applySettings() s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("JavaDir", ui->javaDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); + s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); + // Instance auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) { case Sort_LastLaunch: @@ -247,6 +255,20 @@ void LauncherPage::applySettings() break; } + auto renamingMode = (InstRenamingMode)ui->renamingBehaviorComboBox->currentIndex(); + switch (renamingMode) { + case Rename_Always: + s->set("InstRenamingMode", "MetadataOnly"); + break; + case Rename_Never: + s->set("InstRenamingMode", "PhysicalDir"); + break; + case Rename_Ask: + default: + s->set("InstRenamingMode", "AskEverytime"); + break; + } + // Cat s->set("CatOpacity", ui->catOpacitySpinBox->value()); @@ -277,9 +299,6 @@ void LauncherPage::loadSettings() 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); @@ -302,15 +321,27 @@ void LauncherPage::loadSettings() 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()); + // Instance QString sortMode = s->get("InstSortMode").toString(); - if (sortMode == "LastLaunch") { ui->sortLastLaunchedBtn->setChecked(true); } else { ui->sortByNameBtn->setChecked(true); } + QString renamingMode = s->get("InstRenamingMode").toString(); + InstRenamingMode renamingModeEnum; + if (renamingMode == "MetadataOnly") { + renamingModeEnum = Rename_Always; + } else if (renamingMode == "PhysicalDir") { + renamingModeEnum = Rename_Never; + } else { + renamingModeEnum = Rename_Ask; + } + ui->renamingBehaviorComboBox->setCurrentIndex(renamingModeEnum); + // Cat ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt()); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 3cba468ff..7942f4f4f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -46,317 +46,386 @@ - - - Update Settings + + + Qt::ScrollBarAsNeeded - - - - - Check for updates automatically - - - - - - - - - Update interval - - - - - - - Set it to 0 to only check on launch - - - h - - - 0 - - - 99999999 - - - - - - + + true + + + + + 0 + 0 + 473 + 770 + + + + + + + Update Settings + + + + + + Check for updates automatically + + + + + + + + + Update interval + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 99999999 + + + + + + + + + + + + Folders + + + + + + I&nstances: + + + instDirTextBox + + + + + + + + + + Browse + + + + + + + + Rename instance folders + + + + + + + + Never + + + + + Ask + + + + + Always + + + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + Browse + + + + + + + + &Icons: + + + iconsDirTextBox + + + + + + + + + + Browse + + + + + + + + &Java: + + + javaDirTextBox + + + + + + + + + + Browse + + + + + + + + &Skins: + + + skinsDirTextBox + + + + + + + + + + Browse + + + + + + + + &Downloads: + + + downloadsDirTextBox + + + + + + + + + + Browse + + + + + + + + + + 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 + + + + + + + + + + + + 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 + + + + + + - - - - Folders - - - - - - &Downloads: - - - downloadsDirTextBox - - - - - - - Browse - - - - - - - - - - - - - &Skins: - - - skinsDirTextBox - - - - - - - &Icons: - - - iconsDirTextBox - - - - - - - When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - - - Check downloads folder recursively - - - - - - - - - - &Java: - - - javaDirTextBox - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - Browse - - - - - - - Browse - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - Browse - - - - - - - Browse - - - - - - - - - - Mods - - - - - - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - - Disable using metadata for mods - - - - - - - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - - true - - - - - - - Disable the automatic detection, installation, and updating of mod dependencies. - - - Disable automatic mod dependency management - - - - - - - When creating a new modpack instance, do not suggest updating existing instances instead. - - - Skip modpack update prompt - - - - - - - - - - Miscellaneous - - - - - - 1 - - - - - - - Number of concurrent tasks - - - - - - - 1 - - - - - - - Number of concurrent downloads - - - - - - - Number of manual retries - - - - - - - 0 - - - - - - - Seconds to wait until the requests are terminated - - - Timeout for HTTP requests - - - - - - - s - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - -
@@ -501,36 +570,6 @@ Console - - - - Console Settings - - - - - - Show console while the game is &running - - - - - - - &Automatically close console when the game quits - - - - - - - Show console when the game &crashes - - - - - - @@ -593,7 +632,7 @@ - Qt::ScrollBarAlwaysOff + Qt::ScrollBarAsNeeded false @@ -645,18 +684,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 @@ -668,4 +722,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 3431dcb9c..000000000 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ /dev/null @@ -1,185 +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()); - s->set("UseZink", ui->useZink->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()); - ui->useZink->setChecked(s->get("UseZink").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 7d2741250..000000000 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ /dev/null @@ -1,351 +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>Current fixes include: skin and online mode support.</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 - - - - - - - <html><head/><body><p>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.</p></body></html> - - - Use Zink - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - - - - diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 8f8dab46d..be65e6948 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,16 +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); @@ -99,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() @@ -132,10 +146,7 @@ void ExternalResourcesPage::openedImpl() m_model->startWatching(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } @@ -289,6 +300,16 @@ 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::openPath(m_instance->instanceConfigFolder(), true); @@ -299,23 +320,32 @@ void ExternalResourcesPage::viewFolder() 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 9d6f61db0..5df8aafa2 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -74,7 +74,7 @@ Actions - Qt::ToolButtonTextOnly + Qt::ToolButtonIconOnly true @@ -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,40 +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 - true + false - Export modlist + Export List - Export mod's metadata to text + 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 cf8d86cd4..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ /dev/null @@ -1,610 +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 "minecraft/MinecraftInstance.h" -#include "minecraft/WorldList.h" -#include "settings/Setting.h" -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/java/InstallJavaDialog.h" -#include "ui_InstanceSettingsPage.h" - -#include -#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); - - ui->javaDownloadBtn->setHidden(!BuildConfig.JAVA_DOWNLOADER_ENABLED); - - connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); - connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); - 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); - - auto mInst = dynamic_cast(inst); - m_world_quickplay_supported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); - if (m_world_quickplay_supported) { - auto worlds = mInst->worldList(); - worlds->update(); - for (const auto& world : worlds->allWorlds()) { - ui->worldsCb->addItem(world.folderName()); - } - } else { - ui->worldsCb->hide(); - ui->worldJoinButton->hide(); - ui->serverJoinAddressButton->setChecked(true); - ui->serverJoinAddress->setEnabled(true); - ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); - } - - loadSettings(); - - updateThresholds(); -} - -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"); - } - - // 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()); - m_settings->set("UseZink", ui->useZink->isChecked()); - - } else { - m_settings->reset("EnableFeralGamemode"); - m_settings->reset("EnableMangoHud"); - m_settings->reset("UseDiscreteGpu"); - m_settings->reset("UseZink"); - } - - // 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) { - if (ui->serverJoinAddressButton->isChecked() || !m_world_quickplay_supported) { - m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); - m_settings->reset("JoinWorldOnLaunch"); - } else { - m_settings->set("JoinWorldOnLaunch", ui->worldsCb->currentText()); - m_settings->reset("JoinServerOnLaunchAddress"); - } - } else { - m_settings->reset("JoinServerOnLaunchAddress"); - m_settings->reset("JoinWorldOnLaunch"); - } - - // Use an account for this instance - 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 overrideLocation = m_settings->get("OverrideJavaLocation").toBool(); - bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool(); - - connect(m_settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, - [this] { ui->javaSettingsGroupBox->setChecked(m_settings->get("OverrideJavaLocation").toBool()); }); - ui->javaSettingsGroupBox->setChecked(overrideLocation); - ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); - connect(m_settings->getSetting("JavaPath").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, - [this] { ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); }); - - ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); - - ui->javaArgumentsGroupBox->setChecked(overrideArgs); - 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->lineEditOpenALPath->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()); - ui->useZink->setChecked(m_settings->get("UseZink").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()); - - if (auto server = m_settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { - ui->serverJoinAddress->setText(server); - ui->serverJoinAddressButton->setChecked(true); - ui->worldJoinButton->setChecked(false); - ui->serverJoinAddress->setEnabled(true); - ui->worldsCb->setEnabled(false); - } else if (auto world = m_settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_world_quickplay_supported) { - ui->worldsCb->setCurrentText(world); - ui->serverJoinAddressButton->setChecked(false); - ui->worldJoinButton->setChecked(true); - ui->serverJoinAddress->setEnabled(false); - ui->worldsCb->setEnabled(true); - } else { - ui->serverJoinAddressButton->setChecked(true); - ui->worldJoinButton->setChecked(false); - ui->serverJoinAddress->setEnabled(true); - ui->worldsCb->setEnabled(false); - } - - ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); - updateAccountsMenu(); - - ui->legacySettingsGroupBox->setChecked(m_settings->get("OverrideLegacySettings").toBool()); - ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); -} - -void InstanceSettingsPage::on_javaDownloadBtn_clicked() -{ - auto jdialog = new Java::InstallDialog({}, m_instance, this); - jdialog->exec(); -} - -void InstanceSettingsPage::on_javaDetectBtn_clicked() -{ - if (JavaUtils::getJavaCheckPath().isEmpty()) { - 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); - - if (!java->is_64bit && m_settings->get("MaxMemAlloc").toInt() > 2048) { - CustomMessageBox::selectable(this, tr("Confirm Selection"), - tr("You selected a 32-bit version of Java.\n" - "This installation does not support more than 2048MiB of RAM.\n" - "Please make sure that the maximum memory value is lower."), - QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) - ->exec(); - } - } -} - -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); - } -} - -void InstanceSettingsPage::on_serverJoinAddressButton_toggled(bool checked) -{ - ui->serverJoinAddress->setEnabled(checked); -} - -void InstanceSettingsPage::on_worldJoinButton_toggled(bool checked) -{ - ui->worldsCb->setEnabled(checked); -} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 6499f9e8f..de173937b 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -36,62 +36,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" -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_javaDownloadBtn_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - void on_serverJoinAddressButton_toggled(bool checked); - void on_worldJoinButton_toggled(bool checked); - - void onUseNativeGLFWChanged(bool checked); - void onUseNativeOpenALChanged(bool checked); - - 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; - bool m_world_quickplay_supported; + 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 4905eae87..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ /dev/null @@ -1,814 +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 - - - - - - - - - - - Download Java - - - - - - - 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 - - - - - - - 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 - - - - - - - - - Miscellaneous - - - - - - Legacy settings - - - true - - - 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 - - - Override global game time settings - - - true - - - false - - - - - - Show time spent playing this instance - - - - - - - Record time spent playing this instance - - - - - - - - - - Set a target to join on launch - - - true - - - false - - - - - - Server address: - - - - - - - - - - Singleplayer world - - - - - - - - - - - - - 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 - javaPathTextBox - javaBrowseBtn - javaDownloadBtn - javaDetectBtn - javaTestBtn - skipCompatibilityCheckbox - memoryGroupBox - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaArgumentsGroupBox - jvmArgsTextBox - windowSizeGroupBox - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - consoleSettingsBox - showConsoleCheck - autoCloseConsoleCheck - showConsoleErrorCheck - nativeWorkaroundsGroupBox - useNativeGLFWCheck - useNativeOpenALCheck - showGameTime - recordGameTime - miscellaneousSettingsBox - closeAfterLaunchCheck - quitAfterGameStopCheck - perfomanceGroupBox - enableFeralGamemodeCheck - enableMangoHud - useDiscreteGpuCheck - gameTimeGroupBox - serverJoinGroupBox - serverJoinAddress - instanceAccountGroupBox - instanceAccountSelector - - - -
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 6a4888f9c..7897a2932 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,90 +52,81 @@ #include -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override - { - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); - if (result.isValid()) - return result; + if (result.isValid()) + return result; - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); - - if (result.isValid()) - return result; - - break; - } + break; } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); - return QIdentityProxyModel::data(index, role); + if (result.isValid()) + return result; + + break; + } } - void setFont(QFont font) { m_font = font; } + return QIdentityProxyModel::data(index, role); +} - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const - { - QModelIndex parentIndex = parent(start); - auto compare = [&](int r) -> QModelIndex { - QModelIndex idx = index(r, start.column(), parentIndex); - if (!idx.isValid() || idx == start) { - return QModelIndex(); - } - QVariant v = data(idx, Qt::DisplayRole); - QString t = v.toString(); - if (t.contains(value, Qt::CaseInsensitive)) - return idx; +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { return QModelIndex(); - }; - if (reverse) { - int from = start.row(); - int to = 0; - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r >= to); --r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = rowCount() - 1; - to = start.row(); - } - } else { - int from = start.row(); - int to = rowCount(parentIndex); - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r < to); ++r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = 0; - to = start.row(); - } } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; return QModelIndex(); - } + }; + if (reverse) { + int from = start.row(); + int to = 0; - private: - QFont m_font; -}; + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); +} LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { @@ -189,6 +180,13 @@ void LogPage::modelStateToUI() ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } + if (m_model->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { @@ -202,6 +200,7 @@ void LogPage::UIToModelState() return; } m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } @@ -291,6 +290,14 @@ void LogPage::on_wrapCheckbox_clicked(bool checked) m_model->setLineWrap(checked); } +void LogPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); +} + void LogPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 6c259891d..b4d74fb9c 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include @@ -46,7 +47,18 @@ namespace Ui { class LogPage; } class QTextCharFormat; -class LogFormatProxyModel; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; class LogPage : public QWidget, public BasePage { Q_OBJECT @@ -70,6 +82,7 @@ class LogPage : public QWidget, public BasePage { void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index 31bb368c8..fb8690581 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -74,6 +74,16 @@
+ + + + Color lines + + + true + + + @@ -170,6 +180,7 @@ tabWidget trackLogCheckbox wrapCheckbox + colorCheckbox btnCopy btnPaste btnClear diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index a909d10d1..1738c9cde 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -245,7 +245,6 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin } // MODRINTH - void ModrinthManagedPackPage::parseManagedPack() { qDebug() << "Parsing Modrinth pack"; @@ -338,6 +337,30 @@ void ModrinthManagedPackPage::suggestVersion() ManagedPackPage::suggestVersion(); } +/// @brief Called when the update task has completed. +/// Internally handles the closing of the instance window if the update was successful and shows a message box. +/// @param did_succeed Whether the update task was successful. +void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const +{ + // Close the window if the update was successful + if (did_succeed) { + if (m_instance_window != nullptr) + m_instance_window->close(); + + CustomMessageBox::selectable(nullptr, tr("Update Successful"), + tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), + QMessageBox::Information) + ->show(); + } else { + CustomMessageBox::selectable( + nullptr, tr("Update Failed"), + tr("The instance failed to update to pack version %1. Please check launcher logs for more information.") + .arg(m_inst->getManagedPackVersionName()), + QMessageBox::Critical) + ->show(); + } +} + void ModrinthManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); @@ -363,10 +386,9 @@ void ModrinthManagedPackPage::update() extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); + // Run our task then handle the result auto did_succeed = runUpdateTask(extracted); - - if (m_instance_window && did_succeed) - m_instance_window->close(); + onUpdateTaskCompleted(did_succeed); } void ModrinthManagedPackPage::updateFromFile() @@ -386,14 +408,12 @@ void ModrinthManagedPackPage::updateFromFile() extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); + // Run our task then handle the result auto did_succeed = runUpdateTask(extracted); - - if (m_instance_window && did_succeed) - m_instance_window->close(); + onUpdateTaskCompleted(did_succeed); } // FLAME - FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) : ManagedPackPage(inst, instance_window, parent) { @@ -531,9 +551,7 @@ void FlameManagedPackPage::update() extracted->setConfirmUpdate(false); auto did_succeed = runUpdateTask(extracted); - - if (m_instance_window && did_succeed) - m_instance_window->close(); + onUpdateTaskCompleted(did_succeed); } void FlameManagedPackPage::updateFromFile() @@ -555,8 +573,6 @@ void FlameManagedPackPage::updateFromFile() extracted->setConfirmUpdate(false); auto did_succeed = runUpdateTask(extracted); - - if (m_instance_window && did_succeed) - m_instance_window->close(); + onUpdateTaskCompleted(did_succeed); } #include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index c44f77070..e8d304c6b 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -94,6 +94,8 @@ class ManagedPackPage : public QWidget, public BasePage { BaseInstance* m_inst; bool m_loaded = false; + + void onUpdateTaskCompleted(bool did_succeed) const; }; /** Simple page for when we aren't a managed pack. */ diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp new file mode 100644 index 000000000..11b3a22d1 --- /dev/null +++ b/launcher/ui/pages/instance/McClient.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include + +#include +#include "Json.h" +#include "McClient.h" + +// 7 first bits +#define SEGMENT_BITS 0x7F +// last bit +#define CONTINUE_BIT 0x80 + +McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} + +void McClient::getStatusData() +{ + qDebug() << "Connecting to socket.."; + + connect(&m_socket, &QTcpSocket::connected, this, [this]() { + qDebug() << "Connected to socket successfully"; + sendRequest(); + + connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse); + }); + + connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); }); + + m_socket.connectToHost(m_ip, m_port); +} + +void McClient::sendRequest() +{ + QByteArray data; + writeVarInt(data, 0x00); // packet ID + writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) + writeVarInt(data, m_domain.size()); // server address length + writeString(data, m_domain.toStdString()); // server address + writeFixedInt(data, m_port, 2); // server port + writeVarInt(data, 0x01); // next state + writePacketToSocket(data); // send handshake packet + + writeVarInt(data, 0x00); // packet ID + writePacketToSocket(data); // send status packet +} + +void McClient::readRawResponse() +{ + if (m_responseReadState == 2) { + return; + } + + m_resp.append(m_socket.readAll()); + if (m_responseReadState == 0 && m_resp.size() >= 5) { + m_wantedRespLength = readVarInt(m_resp); + m_responseReadState = 1; + } + + if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { + if (m_resp.size() > m_wantedRespLength) { + qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " + << m_resp.size() << " received)"; + } + parseResponse(); + m_responseReadState = 2; + } +} + +void McClient::parseResponse() +{ + qDebug() << "Received response successfully"; + + int packetID = readVarInt(m_resp); + if (packetID != 0x00) { + throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16)); + } + + Q_UNUSED(readVarInt(m_resp)); // json length + + // 'resp' should now be the JSON string + QJsonDocument doc = QJsonDocument::fromJson(m_resp); + emitSucceed(doc.object()); +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +void McClient::writeVarInt(QByteArray& data, int value) +{ + while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits + // Write 7 bits + data.append((value & SEGMENT_BITS) | CONTINUE_BIT); + + // Erase theses 7 bits from the value to write + // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone + value >>= 7; + } + data.append(value); +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +int McClient::readVarInt(QByteArray& data) +{ + int value = 0; + int position = 0; + char currentByte; + + while (position < 32) { + currentByte = readByte(data); + value |= (currentByte & SEGMENT_BITS) << position; + + if ((currentByte & CONTINUE_BIT) == 0) + break; + + position += 7; + } + + if (position >= 32) + throw Exception("VarInt is too big"); + + return value; +} + +char McClient::readByte(QByteArray& data) +{ + if (data.isEmpty()) { + throw Exception("No more bytes to read"); + } + + char byte = data.at(0); + data.remove(0, 1); + return byte; +} + +// write number with specified size in big endian format +void McClient::writeFixedInt(QByteArray& data, int value, int size) +{ + for (int i = size - 1; i >= 0; i--) { + data.append((value >> (i * 8)) & 0xFF); + } +} + +void McClient::writeString(QByteArray& data, const std::string& value) +{ + data.append(value.c_str()); +} + +void McClient::writePacketToSocket(QByteArray& data) +{ + // we prefix the packet with its length + QByteArray dataWithSize; + writeVarInt(dataWithSize, data.size()); + dataWithSize.append(data); + + // write it to the socket + m_socket.write(dataWithSize); + m_socket.flush(); + + data.clear(); +} + +void McClient::emitFail(QString error) +{ + qDebug() << "Minecraft server ping for status error:" << error; + emit failed(error); + emit finished(); +} + +void McClient::emitSucceed(QJsonObject data) +{ + emit succeeded(data); + emit finished(); +} diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h new file mode 100644 index 000000000..832b70d40 --- /dev/null +++ b/launcher/ui/pages/instance/McClient.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include + +#include + +// Client for the Minecraft protocol +class McClient : public QObject { + Q_OBJECT + + QString m_domain; + QString m_ip; + short m_port; + QTcpSocket m_socket; + + // 0: did not start reading the response yet + // 1: read the response length, still reading the response + // 2: finished reading the response + unsigned m_responseReadState = 0; + unsigned m_wantedRespLength = 0; + QByteArray m_resp; + + public: + explicit McClient(QObject* parent, QString domain, QString ip, short port); + //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data + void getStatusData(); + + private: + void sendRequest(); + //! Accumulate data until we have a full response, then call parseResponse() once + void readRawResponse(); + void parseResponse(); + + void writeVarInt(QByteArray& data, int value); + int readVarInt(QByteArray& data); + char readByte(QByteArray& data); + //! write number with specified size in big endian format + void writeFixedInt(QByteArray& data, int value, int size); + void writeString(QByteArray& data, const std::string& value); + + void writePacketToSocket(QByteArray& data); + + void emitFail(QString error); + void emitSucceed(QJsonObject data); + + signals: + void succeeded(QJsonObject data); + void failed(QString error); + void finished(); +}; diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp new file mode 100644 index 000000000..2a769762c --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include + +#include "McResolver.h" + +McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {} + +void McResolver::ping() +{ + pingWithDomainSRV(m_constrDomain, m_constrPort); +} + +void McResolver::pingWithDomainSRV(QString domain, int port) +{ + QDnsLookup* lookup = new QDnsLookup(this); + lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); + lookup->setType(QDnsLookup::SRV); + + connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() { + QDnsLookup* lookup = qobject_cast(sender()); + + lookup->deleteLater(); + + if (lookup->error() != QDnsLookup::NoError) { + qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString()); + pingWithDomainA(domain, port); + return; + } + + auto records = lookup->serviceRecords(); + if (records.isEmpty()) { + qDebug() << "Warning: no SRV entries found for domain, trying A record lookup"; + pingWithDomainA(domain, port); + return; + } + + const auto& firstRecord = records.at(0); + QString domain = firstRecord.target(); + int port = firstRecord.port(); + pingWithDomainA(domain, port); + }); + + lookup->lookup(); +} + +void McResolver::pingWithDomainA(QString domain, int port) +{ + QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) { + if (hostInfo.error() != QHostInfo::NoError) { + emitFail("A record lookup failed"); + return; + } + + auto records = hostInfo.addresses(); + if (records.isEmpty()) { + emitFail("No A entries found for domain"); + return; + } + + const auto& firstRecord = records.at(0); + emitSucceed(firstRecord.toString(), port); + }); +} + +void McResolver::emitFail(QString error) +{ + qDebug() << "DNS resolver error:" << error; + emit failed(error); + emit finished(); +} + +void McResolver::emitSucceed(QString ip, int port) +{ + emit succeeded(ip, port); + emit finished(); +} diff --git a/launcher/ui/pages/instance/McResolver.h b/launcher/ui/pages/instance/McResolver.h new file mode 100644 index 000000000..3dfeddc6a --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.h @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include + +// resolve the IP and port of a Minecraft server +class McResolver : public QObject { + Q_OBJECT + + QString m_constrDomain; + int m_constrPort; + + public: + explicit McResolver(QObject* parent, QString domain, int port); + void ping(); + + private: + void pingWithDomainSRV(QString domain, int port); + void pingWithDomainA(QString domain, int port); + void emitFail(QString error); + void emitSucceed(QString ip, int port); + + signals: + void succeeded(QString ip, int port); + void failed(QString error); + void finished(); +}; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index a6fbf5088..dad2da8a4 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -48,121 +48,64 @@ #include #include #include +#include #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 changeVersion = new QAction(tr("Change Version"), this); - changeVersion->setToolTip(tr("Change mod version")); - changeVersion->setEnabled(false); - ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, changeVersion); - connect(changeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion); - - ui->actionsToolbar->insertActionAfter(ui->actionVisitItemPage, ui->actionExportMetadata); - connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); - - 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, changeVersion] { - 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")); - } - - changeVersion->setEnabled(mods_list.length() == 1 && mods_list[0]->metadata() != nullptr); - 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 @@ -170,15 +113,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) @@ -193,10 +133,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 @@ -207,9 +147,17 @@ void ModFolderPage::installMods() return; } - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ModFolderPage::downloadDialogFinished(int result) +{ + if (result) { + 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(); @@ -226,8 +174,12 @@ void ModFolderPage::installMods() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); @@ -236,6 +188,8 @@ void ModFolderPage::installMods() m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void ModFolderPage::updateMods(bool includeDeps) @@ -266,12 +220,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()) { @@ -292,7 +246,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(); @@ -321,6 +275,66 @@ void ModFolderPage::updateMods(bool includeDeps) } } +void ModFolderPage::deleteModMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedMods(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 mods.\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 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; + + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata((*mods_list.begin())->metadata()); + m_downloadDialog->open(); +} + +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) { @@ -369,97 +383,3 @@ 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(); - auto selectionCount = m_model->selectedMods(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 mods.\n" - "Are you sure?") - .arg(selectionCount), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - - if (response != QMessageBox::Yes) - return; - } - - m_model->deleteModsMetadata(selection); -} - -void ModFolderPage::changeModVersion() -{ - if (m_instance->typeName() != "Minecraft") - return; // this is a null instance or a legacy instance - - auto profile = static_cast(m_instance)->getPackProfile(); - if (!profile->getModLoaders().has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); - return; - } - if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { - QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); - return; - } - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); - if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) - return; - - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setModMetadata((*mods_list.begin())->metadata()); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - - tasks->deleteLater(); - }); - - for (auto& task : mdownload.getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); - - m_model->update(); - } -} - -void ModFolderPage::exportModMetadata() -{ - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto selectedMods = m_model->selectedMods(selection); - if (selectedMods.length() == 0) - selectedMods = m_model->allMods(); - - std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); - ExportToModListDialog dlg(m_instance->name(), selectedMods, this); - dlg.exec(); -} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 41b605991..8996b1615 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -38,13 +38,15 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" 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,20 +59,21 @@ 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 downloadMods(); + void downloadDialogFinished(int result); + void updateMods(bool includeDeps = false); void deleteModMetadata(); void exportModMetadata(); - - void installMods(); - void updateMods(bool includeDeps = false); - void visitModPages(); void changeModVersion(); protected: std::shared_ptr m_model; + QPointer m_downloadDialog; }; class CoreModFolderPage : public ModFolderPage { diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ab5d98289..afd1ff1c1 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -43,20 +43,44 @@ #include #include +#include +#include +#include #include -#include "RecursiveFileSystemWatcher.h" +#include -OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent) - : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), m_watcher(new RecursiveFileSystemWatcher(this)) +OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) + : QWidget(parent) + , ui(new Ui::OtherLogsPage) + , m_instance(instance) + , m_basePath(instance->gameRoot()) + , m_logSearchPaths(instance->getLogFileSearchPaths()) + , m_model(new LogModel(this)) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - m_watcher->setMatcher(fileFilter); - m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); + m_proxy = new LogFormatProxyModel(this); - connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); - populateSelectLogBox(); + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + m_model->setMaxLines(m_instance->getConsoleMaxLines()); + m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + m_proxy->setSourceModel(m_model.get()); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); @@ -82,29 +106,54 @@ void OtherLogsPage::retranslate() void OtherLogsPage::openedImpl() { - m_watcher->enable(); + const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to start watching" << path; + else + qDebug() << "Started watching" << path; + } + + populateSelectLogBox(); } + void OtherLogsPage::closedImpl() { - m_watcher->disable(); + const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to stop watching" << path; + else + qDebug() << "Stopped watching" << path; + } } void OtherLogsPage::populateSelectLogBox() { + const QString prevCurrentFile = m_currentFile; + + ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); - ui->selectLogBox->addItems(m_watcher->files()); - if (m_currentFile.isEmpty()) { - setControlsEnabled(false); - ui->selectLogBox->setCurrentIndex(-1); - } else { - const int index = ui->selectLogBox->findText(m_currentFile); + ui->selectLogBox->addItems(getPaths()); + ui->selectLogBox->blockSignals(false); + + if (!prevCurrentFile.isEmpty()) { + const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { + ui->selectLogBox->blockSignals(true); ui->selectLogBox->setCurrentIndex(index); + ui->selectLogBox->blockSignals(false); setControlsEnabled(true); + // don't refresh file + return; } else { setControlsEnabled(false); } } + + on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); } void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) @@ -114,7 +163,7 @@ void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) file = ui->selectLogBox->itemText(index); } - if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file))) { + if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); @@ -131,25 +180,19 @@ void OtherLogsPage::on_btnReload_clicked() setControlsEnabled(false); return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); ui->btnReload->setEnabled(true); // allow reload 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) { - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - bool conversionOk = false; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } + auto setPlainText = [this](const QString& text) { QTextDocument* doc = ui->text->document(); - doc->setDefaultFont(QFont(fontFamily, fontSize)); + doc->setDefaultFont(m_proxy->getFont()); 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())); @@ -158,22 +201,65 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - QString content; - if (file.fileName().endsWith(".gz")) { - QByteArray temp; - if (!GZip::unzip(file.readAll(), temp)) { - setPlainText(tr("The file (%1) is not readable.").arg(file.fileName())); - return; + MessageLevel::Enum last = MessageLevel::Unknown; + + auto handleLine = [this, &last](QString line) { + if (line.isEmpty()) + return false; + if (line.back() == '\n') + line = line.remove(line.size() - 1, 1); + MessageLevel::Enum level = MessageLevel::Unknown; + + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if (innerLevel != MessageLevel::Unknown) { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + level = LogParser::guessLevel(line, last); + } + + last = level; + m_model->append(level, line); + return m_model->isOverFlow(); + }; + + // Try to determine a level for each line + ui->text->clear(); + ui->text->setModel(nullptr); + m_model->clear(); + if (file.fileName().endsWith(".gz")) { + QString line; + auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { + auto block = d; + int newlineIndex = block.indexOf('\n'); + while (newlineIndex != -1) { + line += QString::fromUtf8(block).left(newlineIndex); + block.remove(0, newlineIndex + 1); + if (handleLine(line)) { + line.clear(); + return false; + } + line.clear(); + newlineIndex = block.indexOf('\n'); + } + line += QString::fromUtf8(block); + return true; + }); + if (!error.isEmpty()) { + setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); + return; + } else if (!line.isEmpty()) { + handleLine(line); } - content = QString::fromUtf8(temp); } else { - content = QString::fromUtf8(file.readAll()); + while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { + } } - if (content.size() >= 50000000ll) { - showTooBig(); - return; - } - setPlainText(content); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); } } @@ -187,6 +273,11 @@ void OtherLogsPage::on_btnCopy_clicked() GuiUtil::setClipboardText(ui->text->toPlainText()); } +void OtherLogsPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { @@ -201,7 +292,7 @@ void OtherLogsPage::on_btnDelete_clicked() QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (FS::trash(file.fileName())) { return; @@ -214,7 +305,7 @@ void OtherLogsPage::on_btnDelete_clicked() void OtherLogsPage::on_btnClean_clicked() { - auto toDelete = m_watcher->files(); + auto toDelete = getPaths(); if (toDelete.isEmpty()) { return; } @@ -237,7 +328,9 @@ void OtherLogsPage::on_btnClean_clicked() } QStringList failed; for (auto item : toDelete) { - QFile file(FS::PathCombine(m_path, item)); + QString absolutePath = FS::PathCombine(m_basePath, item); + QFile file(absolutePath); + qDebug() << "Deleting log" << absolutePath; if (FS::trash(file.fileName())) { continue; } @@ -263,6 +356,24 @@ void OtherLogsPage::on_btnClean_clicked() } } +void OtherLogsPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!m_model) + return; + m_model->setLineWrap(checked); + ui->text->scrollToBottom(); +} + +void OtherLogsPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); + ui->text->scrollToBottom(); +} + void OtherLogsPage::setControlsEnabled(const bool enabled) { ui->btnReload->setEnabled(enabled); @@ -273,27 +384,44 @@ void OtherLogsPage::setControlsEnabled(const bool enabled) ui->btnClean->setEnabled(enabled); } -// FIXME: HACK, use LogView instead? -static void findNext(QPlainTextEdit* _this, const QString& what, bool reverse) +QStringList OtherLogsPage::getPaths() { - _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); + QDir baseDir(m_basePath); + + QStringList result; + + for (QString searchPath : m_logSearchPaths) { + QDir searchDir(searchPath); + + QStringList filters{ "*.log", "*.log.gz" }; + + if (searchPath != m_basePath) + filters.append("*.txt"); + + QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); + + for (const QString& name : entries) + result.append(baseDir.relativeFilePath(searchDir.filePath(name))); + } + + return result; } void OtherLogsPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); bool reverse = modifiers & Qt::ShiftModifier; - findNext(ui->text, ui->searchBar->text(), reverse); + ui->text->findNext(ui->searchBar->text(), reverse); } void OtherLogsPage::findNextActivated() { - findNext(ui->text, ui->searchBar->text(), false); + ui->text->findNext(ui->searchBar->text(), false); } void OtherLogsPage::findPreviousActivated() { - findNext(ui->text, ui->searchBar->text(), true); + ui->text->findNext(ui->searchBar->text(), true); } void OtherLogsPage::findActivated() diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 85a3a2dbc..70eb145fb 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -39,6 +39,8 @@ #include #include +#include +#include "LogPage.h" #include "ui/pages/BasePage.h" namespace Ui { @@ -51,7 +53,7 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent = 0); + explicit OtherLogsPage(InstancePtr instance, QWidget* parent = 0); ~OtherLogsPage(); QString id() const override { return "logs"; } @@ -71,6 +73,10 @@ class OtherLogsPage : public QWidget, public BasePage { void on_btnCopy_clicked(); void on_btnDelete_clicked(); void on_btnClean_clicked(); + void on_btnBottom_clicked(); + + void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); @@ -80,10 +86,17 @@ class OtherLogsPage : public QWidget, public BasePage { private: void setControlsEnabled(bool enabled); + QStringList getPaths(); + private: Ui::OtherLogsPage* ui; - QString m_path; + InstancePtr m_instance; + /** Path to display log paths relative to. */ + QString m_basePath; + QStringList m_logSearchPaths; QString m_currentFile; - IPathMatcher::Ptr m_fileFilter; - RecursiveFileSystemWatcher* m_watcher; + QFileSystemWatcher m_watcher; + + LogFormatProxyModel* m_proxy; + shared_qobject_ptr m_model; }; diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 3fdb023fe..6d1a46139 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -33,90 +33,6 @@ Tab 1 - - - - - - - Find - - - - - - - false - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Clear the log - - - Delete - - - - - - - Upload the log to the paste service configured in preferences. - - - Upload - - - - - - - Clear the log - - - Clean - - - - - - - Reload - - - - - - - - 0 - 0 - - - - - - @@ -124,12 +40,173 @@ + + + + + + + &Find + + + + + + + Qt::Vertical + + + + + + + Scroll all the way to bottom + + + &Bottom + + + + + + + false + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + + + + 0 + 0 + + + + + + + + Delete the selected log + + + &Delete Selected + + + + + + + Delete all the logs + + + Delete &All + + + + + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + &Upload + + + + + + + Reload the contents of the log from the disk + + + &Reload + + + + + + +
+ + + LogView + QPlainTextEdit +
ui/widgets/LogView.h
+
+
tabWidget selectLogBox @@ -138,6 +215,8 @@ btnPaste btnDelete btnClean + wrapCheckbox + colorCheckbox text searchBar findButton diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 85be64256..f37b3baf9 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -37,43 +37,64 @@ #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); - if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ResourcePackPage::downloadDialogFinished(int result) +{ + if (result) { + 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(); @@ -90,7 +111,92 @@ void ResourcePackPage::downloadRPs() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +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); } @@ -101,3 +207,52 @@ void ResourcePackPage::downloadRPs() 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; + + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index cb84ca96d..e39d417c9 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/ResourcePackFolderModel.h" @@ -58,6 +61,16 @@ 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 downloadDialogFinished(int result); + void updateResourcePacks(); + void deleteResourcePackMetadata(); + void changeResourcePackVersion(); + + protected: + std::shared_ptr m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index b619a07b8..e59002a15 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -150,7 +150,8 @@ class FilterModel : public QIdentityProxyModel { return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); - return result.toString().remove(QRegularExpression("\\.png$")); + static const QRegularExpression s_removeChars("\\.png$"); + return result.toString().remove(s_removeChars); } if (role == Qt::DecorationRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); @@ -559,10 +560,7 @@ void ScreenshotsPage::openedImpl() } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp new file mode 100644 index 000000000..b39f3d117 --- /dev/null +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include "McClient.h" +#include "McResolver.h" +#include "ServerPingTask.h" + +unsigned getOnlinePlayers(QJsonObject data) +{ + return Json::requireInteger(Json::requireObject(data, "players"), "online"); +} + +void ServerPingTask::executeTask() +{ + qDebug() << "Querying status of " << QString("%1:%2").arg(m_domain).arg(m_port); + + // Resolve the actual IP and port for the server + McResolver* resolver = new McResolver(nullptr, m_domain, m_port); + QObject::connect(resolver, &McResolver::succeeded, this, [this, resolver](QString ip, int port) { + qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port; + + // Now that we have the IP and port, query the server + McClient* client = new McClient(nullptr, m_domain, ip, port); + + QObject::connect(client, &McClient::succeeded, this, [this](QJsonObject data) { + m_outputOnlinePlayers = getOnlinePlayers(data); + qDebug() << "Online players: " << m_outputOnlinePlayers; + emitSucceeded(); + }); + QObject::connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); + + // Delete McClient object when done + QObject::connect(client, &McClient::finished, this, [this, client]() { client->deleteLater(); }); + client->getStatusData(); + }); + QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); + + // Delete McResolver object when done + QObject::connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); + resolver->ping(); +} \ No newline at end of file diff --git a/launcher/ui/pages/instance/ServerPingTask.h b/launcher/ui/pages/instance/ServerPingTask.h new file mode 100644 index 000000000..6f03b92ad --- /dev/null +++ b/launcher/ui/pages/instance/ServerPingTask.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include + +class ServerPingTask : public Task { + Q_OBJECT + public: + explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {} + ~ServerPingTask() override = default; + int m_outputOnlinePlayers = -1; + + private: + QString m_domain; + int m_port; + + protected: + virtual void executeTask() override; +}; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d8035e73e..245bbffe2 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -36,6 +36,7 @@ */ #include "ServersPage.h" +#include "ServerPingTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" @@ -48,11 +49,12 @@ #include #include +#include #include #include #include -static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. +static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things. struct Server { // Types @@ -111,9 +113,8 @@ struct Server { // Data - temporary bool m_checked = false; bool m_up = false; - QString m_motd; // https://mctools.org/motd-creator - int m_ping = 0; - int m_currentPlayers = 0; + QString m_motd; // https://mctools.org/motd-creator + std::optional m_currentPlayers; // nullopt if not calculated/calculating int m_maxPlayers = 0; }; @@ -254,11 +255,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row - 1, row); -#else - m_servers.swap(row - 1, row); -#endif endMoveRows(); scheduleSave(); return true; @@ -274,11 +271,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row + 1, row); -#else - m_servers.swap(row + 1, row); -#endif endMoveRows(); scheduleSave(); return true; @@ -296,7 +289,7 @@ class ServersModel : public QAbstractListModel { case 1: return tr("Address"); case 2: - return tr("Latency"); + return tr("Online"); } } @@ -316,10 +309,10 @@ class ServersModel : public QAbstractListModel { if (row < 0 || row >= m_servers.size()) return QVariant(); - switch (column) { - case 0: - switch (role) { - case Qt::DecorationRole: { + switch (role) { + case Qt::DecorationRole: { + switch (column) { + case 0: { auto& bytes = m_servers[row].m_icon; if (bytes.size()) { QPixmap px; @@ -328,27 +321,32 @@ class ServersModel : public QAbstractListModel { } return APPLICATION->getThemedIcon("unknown_server"); } - case Qt::DisplayRole: - return m_servers[row].m_name; - case ServerPtrRole: - return QVariant::fromValue((void*)&m_servers[row]); - default: - return QVariant(); - } - case 1: - switch (role) { - case Qt::DisplayRole: + case 1: return m_servers[row].m_address; default: return QVariant(); } - case 2: - switch (role) { - case Qt::DisplayRole: - return m_servers[row].m_ping; - default: + case 2: + if (role == Qt::DisplayRole) { + if (m_servers[row].m_currentPlayers) { + return *m_servers[row].m_currentPlayers; + } else { + return "..."; + } + } else { return QVariant(); - } + } + } + case Qt::DisplayRole: + if (column == 0) + return m_servers[row].m_name; + else + return QVariant(); + case ServerPtrRole: + if (column == 0) + return QVariant::fromValue((void*)&m_servers[row]); + else + return QVariant(); default: return QVariant(); } @@ -433,6 +431,40 @@ class ServersModel : public QAbstractListModel { } } + void queryServersStatus() + { + // Abort the currently running task if present + if (m_currentQueryTask != nullptr) { + m_currentQueryTask->abort(); + qDebug() << "Aborted previous server query task"; + } + + m_currentQueryTask = ConcurrentTask::Ptr( + new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + int row = 0; + for (Server& server : m_servers) { + // reset current players + server.m_currentPlayers = {}; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + + // Start task to query server status + auto target = MinecraftTarget::parse(server.m_address, false); + auto* task = new ServerPingTask(target.address, target.port); + m_currentQueryTask->addTask(Task::Ptr(task)); + + // Update the model when the task is done + connect(task, &Task::finished, this, [this, task, row]() { + if (m_servers.size() < row) + return; + m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + }); + row++; + } + + m_currentQueryTask->start(); + } + public slots: void dirChanged(const QString& path) { @@ -520,6 +552,7 @@ class ServersModel : public QAbstractListModel { QList m_servers; QFileSystemWatcher* m_watcher = nullptr; QTimer m_saveTimer; + ConcurrentTask::Ptr m_currentQueryTask = nullptr; }; ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) @@ -670,12 +703,12 @@ void ServersPage::openedImpl() m_model->observe(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + + // ping servers + m_model->queryServersStatus(); } void ServersPage::closedImpl() @@ -734,4 +767,9 @@ void ServersPage::on_actionJoin_triggered() APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } +void ServersPage::on_actionRefresh_triggered() +{ + m_model->queryServersStatus(); +} + #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index a27d1d297..77710d6cc 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -85,6 +85,7 @@ class ServersPage : public QMainWindow, public BasePage { void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); + void on_actionRefresh_triggered(); void runningStateChanged(bool running); diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui index e8f79cf2e..d330835c8 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -149,6 +149,8 @@ + + @@ -175,6 +177,11 @@ Join + + + Refresh + + diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 40366a1be..930b0b9da 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -45,27 +45,53 @@ #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); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask(this, "Download Shaders", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ShaderPackPage::downloadDialogFinished(int result) +{ + if (result) { + 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(); @@ -82,7 +108,92 @@ void ShaderPackPage::downloadShaders() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +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); } @@ -93,3 +204,52 @@ void ShaderPackPage::downloadShaders() 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; + + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index d134e67ad..f2b141329 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -37,7 +37,9 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT @@ -53,5 +55,13 @@ class ShaderPackPage : public ExternalResourcesPage { bool shouldDisplay() const override { return true; } public slots: - void downloadShaders(); + void downloadShaderPack(); + void downloadDialogFinished(int result); + void updateShaderPacks(); + void deleteShaderPackMetadata(); + void changeShaderPackVersion(); + + private: + std::shared_ptr m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 7c8d7e061..2886decb4 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -44,38 +44,62 @@ #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); - if (mdownload.exec()) { - auto tasks = - new ConcurrentTask(this, "Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); + m_downloadDialog->open(); +} + +void TexturePackPage::downloadDialogFinished(int result) +{ + if (result) { + 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(); @@ -92,7 +116,92 @@ void TexturePackPage::downloadTPs() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +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); } @@ -103,3 +212,52 @@ void TexturePackPage::downloadTPs() 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; + + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 9c4f24b70..3ebca3e87 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" @@ -55,6 +58,14 @@ 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 downloadDialogFinished(int result); + void updateTexturePacks(); + void deleteTexturePackMetadata(); + void changeTexturePackVersion(); + + private: + std::shared_ptr m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 0c25b4c0c..a1eeb3d25 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -124,10 +124,7 @@ void VersionPage::retranslate() void VersionPage::openedImpl() { auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } @@ -243,7 +240,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()); @@ -252,8 +249,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; @@ -435,7 +435,7 @@ void VersionPage::on_actionDownload_All_triggered() if (updateTasks.isEmpty()) { return; } - auto task = makeShared(this); + auto task = makeShared(); for (auto t : updateTasks) { task->addTask(t); } diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 4ed5f1f73..9e1a0fb55 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -119,10 +119,7 @@ void WorldListPage::openedImpl() } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } @@ -166,12 +163,9 @@ void WorldListPage::retranslate() bool WorldListPage::worldListFilter(QKeyEvent* keyEvent) { - switch (keyEvent->key()) { - case Qt::Key_Delete: - on_actionRemove_triggered(); - return true; - default: - break; + if (keyEvent->key() == Qt::Key_Delete) { + on_actionRemove_triggered(); + return true; } return QWidget::eventFilter(ui->worldTreeView, keyEvent); } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 1f0329321..6e98a88bc 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -39,10 +39,12 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories }; + 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) +ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = *m_packs[entry.row()]; auto profile = static_cast(m_base_instance).getPackProfile(); @@ -60,7 +62,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en return { pack, versions, loaders }; } -ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) { auto& pack = *m_packs[entry.row()]; return { pack }; diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 5c994f373..bb9255cd0 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -39,8 +39,8 @@ class ModModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index c9817cdf7..80d2bcb73 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -59,25 +59,26 @@ namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); - connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } -void ModPage::setFilterWidget(unique_qobject_ptr& widget) +void ModPage::setFilterWidget(ModFilterWidget* widget) { if (m_filter_widget) - disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); + disconnect(m_filter_widget, nullptr, nullptr, nullptr); - auto old = m_ui->splitter->replaceWidget(0, widget.get()); + auto old = m_ui->splitter->replaceWidget(0, widget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } - m_filter_widget.swap(widget); - + m_filter_widget = widget; + if (m_filter_widget) { + m_filter_widget->deleteLater(); + } m_filter = m_filter_widget->getFilter(); - connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); + connect(m_filter_widget, &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); prepareProviderCategories(); } @@ -99,7 +100,7 @@ void ModPage::triggerSearch() updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); - m_fetch_progress.watch(m_model->activeSearchJob().get()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ModPage::urlHandlers() const diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5c9a82303..87865dc83 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -35,8 +35,9 @@ class ModPage : public ResourcePage { page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } @@ -50,11 +51,11 @@ class ModPage : public ResourcePage { void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - virtual unique_qobject_ptr createFilterWidget() = 0; + virtual ModFilterWidget* createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - void setFilterWidget(unique_qobject_ptr&); + void setFilterWidget(ModFilterWidget*); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); @@ -66,7 +67,7 @@ class ModPage : public ResourcePage { void triggerSearch() override; protected: - unique_qobject_ptr m_filter_widget; + ModFilterWidget* m_filter_widget = nullptr; std::shared_ptr m_filter; }; 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 50a170fff..8e3be5e8f 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -82,8 +82,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; - case UserDataTypes::SELECTED: - return pack->isAnyVersionSelected(); + case Qt::CheckStateRole: + return pack->isAnyVersionSelected() ? Qt::Checked : Qt::Unchecked; case UserDataTypes::INSTALLED: return this->isPackInstalled(pack); default: @@ -103,7 +103,6 @@ QHash ResourceModel::roleNames() const roles[Qt::UserRole] = "pack"; roles[UserDataTypes::TITLE] = "title"; roles[UserDataTypes::DESCRIPTION] = "description"; - roles[UserDataTypes::SELECTED] = "selected"; roles[UserDataTypes::INSTALLED] = "installed"; return roles; @@ -193,7 +192,7 @@ void ResourceModel::search() runSearchJob(job); } -void ResourceModel::loadEntry(QModelIndex& entry) +void ResourceModel::loadEntry(const QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; @@ -337,7 +336,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(), &Task::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 }))); @@ -345,7 +344,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) emit dataChanged(index, index, { Qt::DecorationRole }); }); - connect(icon_fetch_action.get(), &Task::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); }); @@ -504,7 +503,7 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind return; } - emit versionListUpdated(); + emit versionListUpdated(index); } void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) @@ -531,7 +530,7 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe return; } - emit projectInfoUpdated(); + emit projectInfoUpdated(index); } void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack, diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 4c7ea33a0..3f1e633ec 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -80,17 +80,17 @@ class ResourceModel : public QAbstractListModel { virtual ResourceAPI::SearchArgs createSearchArguments() = 0; virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } - virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; - virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0; + virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(const QModelIndex&) { return {}; } - virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0; + virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(const QModelIndex&) { return {}; } /** Requests the API for more entries. */ virtual void search(); /** Applies any processing / extra requests needed to fully load the specified entry's information. */ - virtual void loadEntry(QModelIndex&); + virtual void loadEntry(const QModelIndex&); /** Schedule a refresh, clearing the current state. */ void refresh(); @@ -170,8 +170,8 @@ class ResourceModel : public QAbstractListModel { void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); signals: - void versionListUpdated(); - void projectInfoUpdated(); + void versionListUpdated(const QModelIndex& index); + void projectInfoUpdated(const QModelIndex& index); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index d436f320f..0de980ed8 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -20,13 +20,13 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; } -ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.h b/launcher/ui/pages/modplatform/ResourcePackModel.h index e2b4a1957..4f00808e8 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.h +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -31,8 +31,8 @@ class ResourcePackResourceModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index 849ea1111..8a7ed2720 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -14,9 +14,7 @@ namespace ResourceDownload { ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected); -} +{} /******** Callbacks to events in the UI (set up in the derived classes) ********/ @@ -30,7 +28,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 440d91ab0..8d967f73a 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -25,8 +25,9 @@ class ResourcePackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index dea28d6d5..ea8e8d5e9 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -39,14 +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 "StringUtils.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" @@ -54,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); @@ -63,23 +65,28 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); - m_search_timer.setSingleShot(true); + m_searchTimer.setTimerType(Qt::TimerType::CoarseTimer); + m_searchTimer.setSingleShot(true); - connect(&m_search_timer, &QTimer::timeout, this, &ResourcePage::triggerSearch); + connect(&m_searchTimer, &QTimer::timeout, this, &ResourcePage::triggerSearch); // hide progress bar to prevent weird artifact - m_fetch_progress.hide(); - m_fetch_progress.hideIfInactive(true); - m_fetch_progress.setFixedHeight(24); - m_fetch_progress.progressFormat(""); + m_fetchProgress.hide(); + m_fetchProgress.hideIfInactive(true); + m_fetchProgress.setFixedHeight(24); + m_fetchProgress.progressFormat(""); - m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); - m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + auto delegate = new ProjectItemDelegate(this); + m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); + m_ui->packView->viewport()->installEventFilter(this); connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); + + connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePage::onResourceToggle); + connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onResourceToggle); } ResourcePage::~ResourcePage() @@ -120,23 +127,26 @@ 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) { + // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { - onResourceSelected(); - - // To have the 'select mod' button outlined instead of the 'review and confirm' one - m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); - m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason); - + onResourceToggle(m_ui->packView->currentIndex()); keyEvent->accept(); return true; } } + } else if (watched == m_ui->packView->viewport() && event->type() == QEvent::MouseButtonPress) { + auto* mouseEvent = static_cast(event); + + if (mouseEvent->button() == Qt::MiddleButton) { + onResourceToggle(m_ui->packView->indexAt(mouseEvent->pos())); + return true; + } } return QWidget::eventFilter(watched, event); @@ -175,8 +185,11 @@ ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } -void ResourcePage::updateUi() +void ResourcePage::updateUi(const QModelIndex& index) { + if (index != m_ui->packView->currentIndex()) + return; + auto current_pack = getCurrentPack(); if (!current_pack) { m_ui->packDescription->setHtml({}); @@ -247,7 +260,7 @@ void ResourcePage::updateUi() void ResourcePage::updateSelectionButton() { - if (!isOpened || m_selected_version_index < 0) { + if (!isOpened || m_selectedVersionIndex < 0) { m_ui->resourceSelectionButton->setEnabled(false); return; } @@ -257,7 +270,7 @@ void ResourcePage::updateSelectionButton() 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_selected_version_index)) + } 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())); @@ -266,35 +279,48 @@ void ResourcePage::updateSelectionButton() } } -void ResourcePage::updateVersionList() +void ResourcePage::versionListUpdated(const QModelIndex& index) { - auto current_pack = getCurrentPack(); + if (index == m_ui->packView->currentIndex()) { + auto current_pack = getCurrentPack(); - m_ui->versionSelectionBox->blockSignals(true); - m_ui->versionSelectionBox->clear(); - m_ui->versionSelectionBox->blockSignals(false); + m_ui->versionSelectionBox->blockSignals(true); + m_ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->blockSignals(false); - if (current_pack) { - auto installedVersion = m_model->getInstalledPackVersion(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 (!m_model->checkVersionFilters(version)) - continue; + for (int i = 0; i < current_pack->versions.size(); i++) { + auto& version = current_pack->versions[i]; + 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()) - : ""; + 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(QString("%1%2").arg(version.version, release_type), QVariant(i)); + 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 :(")); } - } - 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(); + if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onResourceToggle(index); + } else + updateSelectionButton(); + } else if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onResourceToggle(index); + } } void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) @@ -312,39 +338,49 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI request_load = true; } else { - updateVersionList(); + versionListUpdated(curr); } if (current_pack && !current_pack->extraDataLoaded) request_load = true; + // we are already requesting this + if (m_enableQueue.contains(curr.row())) + request_load = false; + if (request_load) m_model->loadEntry(curr); - updateUi(); + updateUi(curr); } void ResourcePage::onVersionSelectionChanged(int index) { - m_selected_version_index = index; + m_selectedVersionIndex = m_ui->versionSelectionBox->itemData(index).toInt(); updateSelectionButton(); } void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version) { - m_parent_dialog->addResource(pack, version); + m_parentDialog->addResource(pack, version); } void ResourcePage::removeResourceFromDialog(const QString& pack_name) { - m_parent_dialog->removeResource(pack_name); + m_parentDialog->removeResource(pack_name); } void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, 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::modelReset() +{ + m_enableQueue.clear(); } void ResourcePage::removeResourceFromPage(const QString& name) @@ -354,14 +390,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 @@ -377,6 +414,48 @@ void ResourcePage::onResourceSelected() m_ui->packView->repaint(); } +void ResourcePage::onResourceToggle(const QModelIndex& index) +{ + const bool isSelected = index == m_ui->packView->currentIndex(); + auto pack = m_model->data(index, Qt::UserRole).value(); + + if (pack->versionsLoaded) { + if (pack->isAnyVersionSelected()) + removeResourceFromDialog(pack->name); + else { + auto version = std::find_if(pack->versions.begin(), pack->versions.end(), [this](const ModPlatform::IndexedVersion& version) { + return m_model->checkVersionFilters(version); + }); + + if (version == pack->versions.end()) { + auto errorMessage = new QMessageBox( + QMessageBox::Warning, tr("No versions available"), + tr("No versions for '%1' are available.\nThe author likely blocked third-party launchers.").arg(pack->name), + QMessageBox::Ok, this); + + errorMessage->open(); + } else + addResourceToDialog(pack, *version); + } + + if (isSelected) + updateSelectionButton(); + + // force update + QVariant variant; + variant.setValue(pack); + m_model->setData(index, variant, Qt::UserRole); + } else { + // the model is just 1 dimensional so this is fine + m_enableQueue.insert(index.row()); + + // we can't be sure that this hasn't already been requested... + // but this does the job well enough and there's not much point preventing edgecases + if (!isSelected) + m_model->loadEntry(index); + } +} + void ResourcePage::openUrl(const QUrl& url) { // do not allow other url schemes for security reasons @@ -400,14 +479,14 @@ void ResourcePage::openUrl(const QUrl& url) } } - if (!page.isNull() && !m_do_not_jump_to_mod) { + if (!page.isNull() && !m_doNotJumpToMod) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { - m_parent_dialog->selectPage(page); + m_parentDialog->selectPage(page); - auto newPage = m_parent_dialog->selectedPage(); + auto newPage = m_parentDialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; auto model = newPage->m_model; @@ -451,7 +530,7 @@ void ResourcePage::openProject(QVariant projectID) m_ui->resourceFilterButton->hide(); m_ui->packView->hide(); m_ui->resourceSelectionButton->hide(); - m_do_not_jump_to_mod = true; + m_doNotJumpToMod = true; auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -465,20 +544,23 @@ void ResourcePage::openProject(QVariant projectID) auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); cancelBtn->setDefault(false); cancelBtn->setAutoDefault(false); + cancelBtn->setText(tr("Cancel")); connect(okBtn, &QPushButton::clicked, this, [this] { onResourceSelected(); - m_parent_dialog->accept(); + m_parentDialog->accept(); }); - connect(cancelBtn, &QPushButton::clicked, m_parent_dialog, &ResourceDownloadDialog::reject); + connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); - auto jump = [this, okBtn] { + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); + + auto jump = [this] { for (int row = 0; row < m_model->rowCount({}); row++) { const QModelIndex index = m_model->index(row); m_ui->packView->setCurrentIndex(index); - okBtn->setEnabled(true); return; } m_ui->packDescription->setText(tr("The resource was not found")); diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index b625240eb..055db441a 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: @@ -71,15 +71,17 @@ class ResourcePage : public QWidget, public BasePage { void addSortings(); public slots: - virtual void updateUi(); + virtual void updateUi(const QModelIndex& index); virtual void updateSelectionButton(); - virtual void updateVersionList(); + virtual void versionListUpdated(const QModelIndex& index); void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResourceFromDialog(const QString& pack_name); virtual void removeResourceFromPage(const QString& name); virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr); + virtual void modelReset(); + QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } @@ -91,6 +93,7 @@ class ResourcePage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void onResourceSelected(); + void onResourceToggle(const QModelIndex& index); // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 @@ -99,22 +102,24 @@ class ResourcePage : public QWidget, public BasePage { virtual void openUrl(const QUrl&); public: - BaseInstance& m_base_instance; + BaseInstance& m_baseInstance; protected: Ui::ResourcePage* m_ui; - ResourceDownloadDialog* m_parent_dialog = nullptr; + ResourceDownloadDialog* m_parentDialog = nullptr; ResourceModel* m_model = nullptr; - int m_selected_version_index = -1; + int m_selectedVersionIndex = -1; - ProgressWidget m_fetch_progress; + ProgressWidget m_fetchProgress; // Used to do instant searching with a delay to cache quick changes - QTimer m_search_timer; + QTimer m_searchTimer; - bool m_do_not_jump_to_mod = false; + bool m_doNotJumpToMod = false; + + QSet m_enableQueue; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index 8c913657a..efc6bfaf9 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -20,13 +20,13 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; } -ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.h b/launcher/ui/pages/modplatform/ShaderPackModel.h index f3c695e9f..5bb9e58b1 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.h +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -31,8 +31,8 @@ class ShaderPackResourceModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index ebd8d4ea2..ace07db0e 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -8,16 +8,14 @@ #include "ShaderPackModel.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include namespace ResourceDownload { -ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected); -} +ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ @@ -31,7 +29,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 +46,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 4b92c33dc..d436e218a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -25,8 +25,9 @@ class ShaderPackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index cb4cafd41..d56f9334b 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -70,7 +70,7 @@ ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto args = ResourcePackResourceModel::createVersionsArguments(entry); if (!m_version_list->isLoaded()) { diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index 607a03be3..45b5734ee 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -18,7 +18,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { [[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; protected: Meta::VersionList::Ptr m_version_list; diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 42aa921c5..27fd8bcfc 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -27,8 +27,9 @@ class TexturePackResourcePage : public ResourcePackResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } @@ -39,10 +40,7 @@ class TexturePackResourcePage : public ResourcePackResourcePage { [[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); } protected: - TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) - { - connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected); - } + TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) {} }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index f116ca915..e381f2a16 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -82,8 +82,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 6fb867733..5ee8d2c04 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -45,7 +45,9 @@ #include "net/ApiDownload.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods) +AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, + const ATLauncher::PackVersion& version, + QList mods) : QAbstractListModel(parent), m_version(version), m_mods(mods) { // fill mod index @@ -62,9 +64,9 @@ AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::Pa } } -QVector AtlOptionalModListModel::getResult() +QList AtlOptionalModListModel::getResult() { - QVector result; + QList result; for (const auto& mod : m_mods) { if (m_selection[mod.name]) { @@ -233,7 +235,7 @@ void AtlOptionalModListModel::clearAll() emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } -void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) +void AtlOptionalModListModel::toggleMod(const ATLauncher::VersionMod& mod, int index) { auto enable = !m_selection[mod.name]; @@ -251,7 +253,7 @@ void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) setMod(mod, index, enable); } -void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) +void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit) { if (m_selection[mod.name] == enable) return; @@ -313,7 +315,7 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } } -AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods) +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods) : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { ui->setupUi(this); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 767d277d9..fa39e997c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -55,9 +55,9 @@ class AtlOptionalModListModel : public QAbstractListModel { DescriptionColumn, }; - AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods); + AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); - QVector getResult(); + QList getResult(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; @@ -78,29 +78,29 @@ class AtlOptionalModListModel : public QAbstractListModel { void clearAll(); private: - void toggleMod(ATLauncher::VersionMod mod, int index); - void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + void toggleMod(const ATLauncher::VersionMod& mod, int index); + void setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit = true); private: NetJob::Ptr m_jobPtr; std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; - QVector m_mods; + QList m_mods; QMap m_selection; QMap m_index; - QMap> m_dependents; + QMap> m_dependents; }; class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods); + AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); ~AtlOptionalModDialog() override; - QVector getResult() { return listModel->getResult(); } + QList getResult() { return listModel->getResult(); } void useShareCode(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp index 0c7257859..dc9a4758f 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp @@ -41,8 +41,8 @@ AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {} -std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version, - QVector mods) +std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version, + QList mods) { AtlOptionalModDialog optionalModDialog(m_parent, version, mods); auto result = optionalModDialog.exec(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h index 52ced2615..99f907a19 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h @@ -48,7 +48,7 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInt private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; - std::optional> chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + std::optional> chooseOptionalMods(const ATLauncher::PackVersion& version, QList mods) override; void displayMessage(QString message) override; private: diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index cfdb185ff..d501bf9f4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -65,8 +65,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -189,7 +187,7 @@ void ListModel::performPaginatedSearch() 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->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 9abf4a9c6..bcbae0d76 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -273,7 +273,7 @@ void FlamePage::suggestCurrent() void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; - ui->versionSelectionBox->currentData().toInt(&is_blocked); + ui->versionSelectionBox->itemData(index).toInt(&is_blocked); if (index == -1 || is_blocked) { m_selected_version_index = -1; @@ -341,17 +341,20 @@ void FlamePage::setSearchTerm(QString term) void FlamePage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, false, this); - m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto widget = new ModFilterWidget(nullptr, false, this); + if (m_filterWidget) { + m_filterWidget->deleteLater(); + } + m_filterWidget = (widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + connect(m_filterWidget, &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]() { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 27c96d2f1..a828a2a29 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -100,6 +100,6 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + ModFilterWidget* 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 b028e7569..cf882ef1c 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -32,16 +32,16 @@ - - - Search and filter... + + + Filter options - - - Filter + + + Search and filter... diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index ae4562be4..31ddefebf 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -32,7 +32,7 @@ void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + FlameMod::loadIndexedPackVersions(m, arr); } auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion @@ -65,7 +65,7 @@ void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJso void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + FlameMod::loadIndexedPackVersions(m, arr); } bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const @@ -93,9 +93,9 @@ void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJson void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + FlameMod::loadIndexedPackVersions(m, arr); - QVector filtered_versions(m.versions.size()); + QList filtered_versions(m.versions.size()); // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. for (auto const& version : m.versions) { @@ -122,7 +122,7 @@ ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(const QModelIndex& entry) { auto args = TexturePackResourceModel::createVersionsArguments(entry); @@ -157,7 +157,7 @@ void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonO void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + FlameMod::loadIndexedPackVersions(m, arr); } bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 458fd85d0..9b86a0944 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -69,7 +69,7 @@ class FlameTexturePackModel : public TexturePackResourceModel { void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index ce8c03fa1..bfe873ac8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -207,9 +207,9 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr FlameModPage::createFilterWidget() +ModFilterWidget* FlameModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_base_instance), false, this); + return new ModFilterWidget(&static_cast(m_baseInstance), false, this); } void FlameModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 052706549..3117851a5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -96,7 +96,7 @@ class FlameModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; - unique_qobject_ptr createFilterWidget() override; + ModFilterWidget* createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 18c604ca4..337c3e474 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -13,6 +13,11 @@ + + + true + + Note: If your FTB instances are not in the default location, select it using the button next to search. diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index f3c737977..06836c3c5 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -106,9 +106,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } auto pack = m_modpacks.at(pos); - if (role == Qt::ToolTipRole) { - } - switch (role) { case Qt::ToolTipRole: return tr("Minecraft %1").arg(pack.mcVersion); @@ -128,8 +125,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return tr("Minecraft %1").arg(pack.mcVersion); - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 98922123c..b68fcd34a 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -195,8 +195,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -213,7 +211,7 @@ void ListModel::fill(ModpackList modpacks_) endResetModel(); } -void ListModel::addPack(Modpack modpack) +void ListModel::addPack(const Modpack& modpack) { beginResetModel(); this->modpacks.append(modpack); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h index f35012078..e4477c929 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -62,7 +62,7 @@ class ListModel : public QAbstractListModel { Qt::ItemFlags flags(const QModelIndex& index) const override; void fill(ModpackList modpacks); - void addPack(Modpack modpack); + void addPack(const Modpack& modpack); void clear(); void remove(int row); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 226a30ee3..5752b6c61 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -213,7 +213,7 @@ void Page::ftbPackDataDownloadAborted() CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); } -void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) +void Page::ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack) { privateListModel->addPack(pack); } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index a2dee24e9..818000c05 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -85,7 +85,7 @@ class Page : public QWidget, public ModpackProviderBasePage { void ftbPackDataDownloadFailed(QString reason); void ftbPackDataDownloadAborted(); - void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); + void ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); void onSortingSelectionChanged(QString data); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 417ff4080..4681b1a7f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -106,8 +106,6 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -155,7 +153,7 @@ void ModpackListModel::performPaginatedSearch() 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->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 8803c6dd9..784b656a1 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -375,7 +375,7 @@ void ModrinthPage::onVersionSelectionChanged(int index) selectedVersion = ""; return; } - selectedVersion = ui->versionSelectionBox->currentData().toString(); + selectedVersion = ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } @@ -391,17 +391,19 @@ QString ModrinthPage::getSerachTerm() const void ModrinthPage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, true, this); - m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto widget = new ModFilterWidget(nullptr, true, this); + if (m_filterWidget) + m_filterWidget->deleteLater(); + m_filterWidget = widget; + auto old = ui->splitter->replaceWidget(0, m_filterWidget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + connect(m_filterWidget, &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 7f504cdbd..c90402f52 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -103,6 +103,6 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + ModFilterWidget* 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 ef44abb52..d6e983929 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -14,16 +14,16 @@ - - - Search and filter... + + + Filter options - - - Filter + + + Search and filter... diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 856018294..213d6e39e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -39,7 +39,7 @@ void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObjec void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr); } auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion @@ -66,7 +66,7 @@ void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, Q void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr); } auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -88,7 +88,7 @@ void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJ void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr); } auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -110,7 +110,7 @@ void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJs void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr); } auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 85dcde471..38a750622 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -142,9 +142,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr ModrinthModPage::createFilterWidget() +ModFilterWidget* ModrinthModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_base_instance), true, this); + return new ModFilterWidget(&static_cast(m_baseInstance), true, this); } void ModrinthModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index eaf6129a5..e2ad60b51 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -94,7 +94,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - unique_qobject_ptr createFilterWidget() override; + ModFilterWidget* createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index fc7fa4d39..11d57f071 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -37,7 +37,6 @@ #include #include -#include namespace Technic { struct Modpack { @@ -61,7 +60,7 @@ struct Modpack { bool versionsLoaded = false; QString recommended; - QVector versions; + QList versions; }; } // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index f7e7f4433..c689ab0d2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -89,8 +89,6 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 8caae173c..6b8ece9f7 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -14,7 +14,7 @@ #include "JavaCommon.h" -#include "ui/widgets/JavaSettingsWidget.h" +#include "ui/widgets/JavaWizardWidget.h" #include "ui/widgets/VersionSelectWidget.h" JavaWizardPage::JavaWizardPage(QWidget* parent) : BaseWizardPage(parent) @@ -27,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); @@ -58,13 +58,13 @@ bool JavaWizardPage::validatePage() 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()); diff --git a/launcher/ui/setupwizard/JavaWizardPage.h b/launcher/ui/setupwizard/JavaWizardPage.h index 9bf9cfa2b..914630d0b 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.h +++ b/launcher/ui/setupwizard/JavaWizardPage.h @@ -2,7 +2,7 @@ #include "BaseWizardPage.h" -class JavaSettingsWidget; +class JavaWizardWidget; class JavaWizardPage : public BaseWizardPage { Q_OBJECT @@ -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/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/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 081ba1900..b8c5738b7 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -181,7 +181,7 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) 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 readColor = [](const QJsonObject& colors, const QString& colorName) -> QColor { auto colorValue = Json::ensureString(colors, colorName, QString()); if (!colorValue.isEmpty()) { QColor color(colorValue); @@ -196,7 +196,7 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) if (root.contains("colors")) { auto colorsRoot = Json::requireObject(root, "colors"); - auto readAndSetPaletteColor = [&](QPalette::ColorRole role, const QString& colorName) { + auto readAndSetPaletteColor = [this, readColor, colorsRoot](QPalette::ColorRole role, const QString& colorName) { auto color = readColor(colorsRoot, colorName); if (color.isValid()) { m_palette.setColor(role, color); @@ -229,7 +229,7 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) hasCustomLogColors = true; auto logColorsRoot = Json::requireObject(root, "logColors"); - auto readAndSetLogColor = [&](MessageLevel::Enum level, bool fg, const QString& colorName) { + auto readAndSetLogColor = [this, readColor, logColorsRoot](MessageLevel::Enum level, bool fg, const QString& colorName) { auto color = readColor(logColorsRoot, colorName); if (color.isValid()) { if (fg) diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index 80e821349..f5b8232a8 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -18,6 +18,11 @@ #include "HintOverrideProxyStyle.h" +HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) +{ + setObjectName(style->objectName()); +} + int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, diff --git a/launcher/ui/themes/HintOverrideProxyStyle.h b/launcher/ui/themes/HintOverrideProxyStyle.h index 09b296018..e9c489d09 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.h +++ b/launcher/ui/themes/HintOverrideProxyStyle.h @@ -25,7 +25,7 @@ class HintOverrideProxyStyle : public QProxyStyle { Q_OBJECT public: - HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {} + explicit HintOverrideProxyStyle(QStyle* style); int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index a1674455a..c9b2e5cfd 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -40,18 +40,29 @@ #include "HintOverrideProxyStyle.h" #include "ThemeManager.h" -SystemTheme::SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme) +// See https://github.com/MultiMC/Launcher/issues/1790 +// or https://github.com/PrismLauncher/PrismLauncher/issues/490 +static const QStringList S_NATIVE_STYLES{ "windows11", "windowsvista", "macos", "system", "windows" }; + +SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme) { - themeName = isDefaultTheme ? "system" : styleName; - widgetTheme = styleName; - colorPalette = palette; + m_themeName = isDefaultTheme ? "system" : styleName; + m_widgetTheme = styleName; + // NOTE: SystemTheme is reconstructed on page refresh. We can't accurately determine the system palette here + // See also S_NATIVE_STYLES comment + if (S_NATIVE_STYLES.contains(m_themeName)) { + m_colorPalette = defaultPalette; + } else { + auto style = QStyleFactory::create(styleName); + m_colorPalette = style != nullptr ? style->standardPalette() : defaultPalette; + delete style; + } } void SystemTheme::apply(bool initial) { - // See https://github.com/MultiMC/Launcher/issues/1790 - // or https://github.com/PrismLauncher/PrismLauncher/issues/490 - if (initial) { + // See S_NATIVE_STYLES comment + if (initial && S_NATIVE_STYLES.contains(m_themeName)) { QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); return; } @@ -61,35 +72,35 @@ void SystemTheme::apply(bool initial) QString SystemTheme::id() { - return themeName; + return m_themeName; } QString SystemTheme::name() { - if (themeName.toLower() == "windowsvista") { + if (m_themeName.toLower() == "windowsvista") { return QObject::tr("Windows Vista"); - } else if (themeName.toLower() == "windows") { + } else if (m_themeName.toLower() == "windows") { return QObject::tr("Windows 9x"); - } else if (themeName.toLower() == "windows11") { + } else if (m_themeName.toLower() == "windows11") { return QObject::tr("Windows 11"); - } else if (themeName.toLower() == "system") { + } else if (m_themeName.toLower() == "system") { return QObject::tr("System"); } else { - return themeName; + return m_themeName; } } QString SystemTheme::tooltip() { - if (themeName.toLower() == "windowsvista") { + if (m_themeName.toLower() == "windowsvista") { return QObject::tr("Widget style trying to look like your win32 theme"); - } else if (themeName.toLower() == "windows") { + } else if (m_themeName.toLower() == "windows") { return QObject::tr("Windows 9x inspired widget style"); - } else if (themeName.toLower() == "windows11") { + } else if (m_themeName.toLower() == "windows11") { return QObject::tr("WinUI 3 inspired Qt widget style"); - } else if (themeName.toLower() == "fusion") { + } else if (m_themeName.toLower() == "fusion") { return QObject::tr("The default Qt widget style"); - } else if (themeName.toLower() == "system") { + } else if (m_themeName.toLower() == "system") { return QObject::tr("Your current system theme"); } else { return ""; @@ -98,12 +109,12 @@ QString SystemTheme::tooltip() QString SystemTheme::qtTheme() { - return widgetTheme; + return m_widgetTheme; } QPalette SystemTheme::colorScheme() { - return colorPalette; + return m_colorPalette; } QString SystemTheme::appStyleSheet() diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index 7c260fdc4..7ae24c3db 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -38,7 +38,7 @@ class SystemTheme : public ITheme { public: - SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme); + SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme); virtual ~SystemTheme() {} void apply(bool initial) override; @@ -53,7 +53,7 @@ class SystemTheme : public ITheme { QColor fadeColor() override; private: - QPalette colorPalette; - QString widgetTheme; - QString themeName; + QPalette m_colorPalette; + QString m_widgetTheme; + QString m_themeName; }; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 691a51668..30a1fe7be 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -36,6 +36,9 @@ ThemeManager::ThemeManager() { + QIcon::setFallbackThemeName(QIcon::themeName()); + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path()); + themeDebugLog() << "Determining System Widget Theme..."; const auto& style = QApplication::style(); m_defaultStyle = style->objectName(); @@ -93,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()) { @@ -348,4 +347,4 @@ void ThemeManager::refresh() initializeThemes(); initializeCatPacks(); -} \ No newline at end of file +} diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index a9036107c..8de7562d1 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -68,8 +68,8 @@ class ThemeManager { QDir m_applicationThemeFolder{ "themes" }; QDir m_catPacksFolder{ "catpacks" }; std::map> m_catPacks; - QString m_defaultStyle; QPalette m_defaultPalette; + QString m_defaultStyle; LogColors m_logColors; void initializeThemes(); diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp index 41def3ba1..02b629162 100644 --- a/launcher/ui/widgets/CheckComboBox.cpp +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -40,7 +40,7 @@ class CheckComboModel : public QIdentityProxyModel { { if (role == Qt::CheckStateRole) { auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); - return checked.contains(txt) ? Qt::Checked : Qt::Unchecked; + return m_checked.contains(txt) ? Qt::Checked : Qt::Unchecked; } if (role == Qt::DisplayRole) return QIdentityProxyModel::data(index, Qt::DisplayRole); @@ -50,10 +50,10 @@ class CheckComboModel : public QIdentityProxyModel { { if (role == Qt::CheckStateRole) { auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); - if (checked.contains(txt)) { - checked.removeOne(txt); + if (m_checked.contains(txt)) { + m_checked.removeOne(txt); } else { - checked.push_back(txt); + m_checked.push_back(txt); } emit dataChanged(index, index); emit checkStateChanged(); @@ -61,13 +61,13 @@ class CheckComboModel : public QIdentityProxyModel { } return QIdentityProxyModel::setData(index, value, role); } - QStringList getChecked() { return checked; } + QStringList getChecked() { return m_checked; } signals: void checkStateChanged(); private: - QStringList checked; + QStringList m_checked; }; CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ") @@ -92,7 +92,7 @@ void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) void CheckComboBox::hidePopup() { - if (!containerMousePress) + if (!m_containerMousePress) QComboBox::hidePopup(); } @@ -138,7 +138,7 @@ bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event) } case QEvent::MouseButtonPress: { auto ev = static_cast(event); - containerMousePress = ev && view()->indexAt(ev->pos()).isValid(); + m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid(); break; } case QEvent::Wheel: diff --git a/launcher/ui/widgets/CheckComboBox.h b/launcher/ui/widgets/CheckComboBox.h index 876c6e3e1..469587762 100644 --- a/launcher/ui/widgets/CheckComboBox.h +++ b/launcher/ui/widgets/CheckComboBox.h @@ -60,5 +60,5 @@ class CheckComboBox : public QComboBox { private: QString m_default_text; QString m_separator; - bool containerMousePress; + bool m_containerMousePress = false; }; \ No newline at end of file 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/FocusLineEdit.h b/launcher/ui/widgets/FocusLineEdit.h index f5ea6602e..797969406 100644 --- a/launcher/ui/widgets/FocusLineEdit.h +++ b/launcher/ui/widgets/FocusLineEdit.h @@ -1,3 +1,5 @@ +#pragma once + #include class FocusLineEdit : public QLineEdit { diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 44f702659..3ef5dcb88 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -84,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 @@ -93,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(", "); @@ -145,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(); } @@ -209,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 })); } diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 2b270c482..a255168e9 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -1,559 +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 -#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 #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"); - m_memoryTimer = new QTimer(this); - 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(onSpinBoxValueChanged(int))); - connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_memoryTimer, &QTimer::timeout, this, &JavaSettingsWidget::memoryValueChanged); - connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected); - connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked); - connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited); - connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaSettingsWidget::javaDownloadBtn_clicked); - } -} + 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); + 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")); - - 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_ui->javaDownloadBtn, &QPushButton::clicked, this, [this] { + auto javaDialog = new Java::InstallDialog({}, m_instance.get(), this); + javaDialog->exec(); }); - - 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); + connect(m_ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { + if (m_instance->settings()->get("JavaPath").toString() != newValue) { + m_instance->settings()->set("AutomaticJava", false); } }); } - m_verticalLayout->addWidget(m_autoJavaGroupBox); - m_verticalLayout->addLayout(m_horizontalBtnLayout); + 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_verticalLayout->addWidget(m_versionWidget); - m_verticalLayout->addLayout(m_horizontalLayout); - m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); - 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(); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_autodownloadCheckBox->setChecked(true); - } -} - -void JavaSettingsWidget::refresh() -{ - if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { - return; - } - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; - } - m_versionWidget->loadList(); -} - -JavaSettingsWidget::ValidationStatus JavaSettingsWidget::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 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() -{ - 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 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() -{ - 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 JavaSettingsWidget::javaDownloadBtn_clicked() -{ - auto jdialog = new Java::InstallDialog({}, nullptr, this); - jdialog->exec(); -} - -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(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0, this)); - connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); - m_checker->start(); -} - -void JavaSettingsWidget::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 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")); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); - } - m_autodetectJavaCheckBox->setText(tr("Autodetect Java version")); - m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); -} - -void JavaSettingsWidget::updateThresholds() -{ - 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 JavaSettingsWidget::autoDownloadJava() const -{ - return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); -} - -bool JavaSettingsWidget::autoDetectJava() const -{ - return m_autodetectJavaCheckBox->isChecked(); -} - -void JavaSettingsWidget::onSpinBoxValueChanged(int) -{ - m_memoryTimer->start(500); } JavaSettingsWidget::~JavaSettingsWidget() { - delete m_verticalSpacer; -}; \ No newline at end of file + 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; + } + + 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(); + } + } +} +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 (maxMem >= sysMiB) { + iconName = "status-bad"; + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (maxMem > (sysMiB * 0.9)) { + iconName = "status-yellow"; + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { + iconName = "status-yellow"; + m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + } else { + iconName = "status-good"; + m_ui->labelMaxMemIcon->setToolTip(""); + } + + { + auto height = m_ui->labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = APPLICATION->getThemedIcon(iconName); + QPixmap pix = icon.pixmap(height, height); + m_ui->labelMaxMemIcon->setPixmap(pix); + } +} diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 35be48478..21a71fb8b 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -1,106 +1,68 @@ -#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; - -/** - * 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; - bool autoDetectJava() const; - bool autoDownloadJava() const; + void loadSettings(); + void saveSettings(); + private slots: + void onJavaBrowse(); + void onJavaAutodetect(); + void onJavaTest(); 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; + 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/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 6578b1f12..df25a2434 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -42,6 +42,7 @@ LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) { setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); m_defaultFormat = new QTextCharFormat(currentCharFormat()); + setUndoRedoEnabled(false); } LogView::~LogView() @@ -60,6 +61,14 @@ void LogView::setWordWrap(bool wrapping) } } +void LogView::setColorLines(bool colorLines) +{ + if (m_colorLines == colorLines) + return; + m_colorLines = colorLines; + repopulate(); +} + void LogView::setModel(QAbstractItemModel* model) { if (m_model) { @@ -121,6 +130,8 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) QTextDocument document; QTextCursor cursor(&document); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); for (int i = first; i <= last; i++) { auto idx = m_model->index(i, 0, parent); auto text = m_model->data(idx, Qt::DisplayRole).toString(); @@ -130,17 +141,17 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) format.setFont(font.value()); } auto fg = m_model->data(idx, Qt::ForegroundRole); - if (fg.isValid()) { + if (fg.isValid() && m_colorLines) { format.setForeground(fg.value()); } auto bg = m_model->data(idx, Qt::BackgroundRole); - if (bg.isValid()) { + if (bg.isValid() && m_colorLines) { format.setBackground(bg.value()); } - cursor.movePosition(QTextCursor::End); cursor.insertText(text, format); cursor.insertBlock(); } + cursor.endEditBlock(); QTextDocumentFragment fragment(&document); QTextCursor workCursor = textCursor(); diff --git a/launcher/ui/widgets/LogView.h b/launcher/ui/widgets/LogView.h index dde5f8f76..69ca332bb 100644 --- a/launcher/ui/widgets/LogView.h +++ b/launcher/ui/widgets/LogView.h @@ -15,6 +15,7 @@ class LogView : public QPlainTextEdit { public slots: void setWordWrap(bool wrapping); + void setColorLines(bool colorLines); void findNext(const QString& what, bool reverse); void scrollToBottom(); @@ -32,4 +33,5 @@ class LogView : public QPlainTextEdit { QTextCharFormat* m_defaultFormat = nullptr; bool m_scroll = false; bool m_scrolling = false; + bool m_colorLines = true; }; 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 68adcdb71..8be3ce8d3 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -49,11 +49,6 @@ #include "Application.h" #include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) -{ - return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); -} - class VersionBasicModel : public QIdentityProxyModel { Q_OBJECT @@ -134,6 +129,7 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi ui->versions->hide(); ui->showAllVersions->hide(); ui->environmentGroup->hide(); + ui->openSource->hide(); } ui->versions->setStyleSheet("combobox-popup: 0;"); @@ -147,11 +143,10 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi 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->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + else + ui->liteLoader->setVisible(false); if (extended) { connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); @@ -159,6 +154,12 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi } 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(); @@ -208,6 +209,7 @@ void ModFilterWidget::loadVersionList() void ModFilterWidget::prepareBasicFilter() { + m_filter->openSource = false; if (m_instance) { m_filter->hideInstalled = false; m_filter->side = ""; // or "both" @@ -216,6 +218,7 @@ void ModFilterWidget::prepareBasicFilter() ui->forge->setChecked(loaders & ModPlatform::Forge); ui->fabric->setChecked(loaders & ModPlatform::Fabric); ui->quilt->setChecked(loaders & ModPlatform::Quilt); + ui->liteLoader->setChecked(loaders & ModPlatform::LiteLoader); m_filter->loaders = loaders; auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); m_filter->versions.emplace_front(def); @@ -261,6 +264,8 @@ void ModFilterWidget::onLoadersFilterChanged() loaders |= ModPlatform::Fabric; if (ui->quilt->isChecked()) loaders |= ModPlatform::Quilt; + if (ui->liteLoader->isChecked()) + loaders |= ModPlatform::LiteLoader; m_filter_changed = loaders != m_filter->loaders; m_filter->loaders = loaders; if (m_filter_changed) @@ -271,13 +276,13 @@ void ModFilterWidget::onSideFilterChanged() { QString side; - if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) { - if (ui->clientSide->isChecked()) - side = "client"; - else - side = "server"; + 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 { - // both are checked or none are checked; in either case no filtering will happen side = ""; } @@ -337,4 +342,30 @@ void ModFilterWidget::setCategories(const QList& categori } } -#include "ModFilterWidget.moc" \ No newline at end of file +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 50f0e06c9..c7192a0d6 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -64,11 +64,12 @@ class ModFilterWidget : public QTabWidget { QString side; bool hideInstalled; QStringList categoryIds; + bool openSource; 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; + releases == other.releases && categoryIds == other.categoryIds && openSource == other.openSource; } bool operator!=(const Filter& other) const { return !(*this == other); } @@ -82,7 +83,7 @@ class ModFilterWidget : public QTabWidget { } }; - static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; @@ -95,8 +96,6 @@ class ModFilterWidget : public QTabWidget { void setCategories(const QList&); private: - ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); - void loadVersionList(); void prepareBasicFilter(); @@ -107,6 +106,8 @@ class ModFilterWidget : public QTabWidget { void onSideFilterChanged(); void onHideInstalledFilterChanged(); void onShowAllVersionsChanged(); + void onOpenSourceFilterChanged(); + void onReleaseFilterChanged(); private: Ui::ModFilterWidget* ui; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index 236847094..788202714 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -63,8 +63,8 @@ 0 0 - 308 - 598 + 294 + 781 @@ -121,6 +121,13 @@
+ + + + LiteLoader + + +
@@ -188,6 +195,50 @@
+ + + + Open source only + + + + + + + Release type + + + + + + Release + + + + + + + Beta + + + + + + + Alpha + + + + + + + Unknown + + + + + + diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 6946df41f..03fa659c9 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -1,9 +1,11 @@ #include "ProjectItem.h" -#include "Common.h" +#include +#include #include #include +#include "Common.h" ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} @@ -14,13 +16,20 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QStyleOptionViewItem opt(option); initStyleOption(&opt, index); + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + auto rect = opt.rect; - if (opt.state & QStyle::State_Selected) { - painter->fillRect(rect, opt.palette.highlight()); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); + + if (option.state & QStyle::State_Selected && style->objectName() != "windowsvista") painter->setPen(opt.palette.highlightedText().color()); - } else if (opt.state & QStyle::State_MouseOver) { - painter->fillRect(rect, opt.palette.window()); + + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { + QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); + style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget); + + rect.setX(checkboxOpt.rect.right()); } // The default icon size will be a square (and height is usually the lower value). @@ -42,6 +51,9 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o int x = rect.x() + icon_x_margin; int y = rect.y() + icon_y_margin; + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) + rect.translate(icon_x_margin / 2, 0); + // Prevent 'scaling null pixmap' warnings if (icon_width > 0 && icon_height > 0) opt.icon.paint(painter, x, y, icon_width, icon_height); @@ -56,26 +68,12 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); + if (index.data(UserDataTypes::INSTALLED).toBool()) + title = tr("%1 [installed]").arg(title); + painter->save(); auto font = opt.font; - if (index.data(UserDataTypes::SELECTED).toBool()) { - // Set nice font - font.setBold(true); - font.setUnderline(true); - } - if (index.data(UserDataTypes::INSTALLED).toBool()) { - auto hRect = opt.rect; - hRect.setX(hRect.x() + 1); - hRect.setY(hRect.y() + 1); - hRect.setHeight(hRect.height() - 2); - hRect.setWidth(hRect.width() - 2); - // Set nice font - font.setItalic(true); - font.setOverline(true); - painter->drawRect(hRect); - } - font.setPointSize(font.pointSize() + 2); painter->setFont(font); @@ -132,3 +130,56 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->restore(); } + +bool ProjectItemDelegate::editorEvent(QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) +{ + if (!(event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick)) + return false; + + auto mouseEvent = (QMouseEvent*)event; + + if (mouseEvent->button() != Qt::LeftButton) + return false; + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + + const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); + + if (!checkboxOpt.rect.contains(mouseEvent->pos().x(), mouseEvent->pos().y())) + return false; + + // swallow other events + // (prevents item being selected or double click action triggering) + if (event->type() != QEvent::MouseButtonRelease) + return true; + + emit checkboxClicked(index); + return true; +} + +QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const +{ + QStyleOptionViewItem checkboxOpt = opt; + + checkboxOpt.state &= ~QStyle::State_HasFocus; + + if (checkboxOpt.checkState == Qt::Checked) + checkboxOpt.state |= QStyle::State_On; + else + checkboxOpt.state |= QStyle::State_Off; + + QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); + // 5px is the typical top margin for image + // we don't want the checkboxes to be all over the place :) + checkboxOpt.rect = QRect(opt.rect.x() + 5, opt.rect.y() + (opt.rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), + checkboxRect.height()); + + return checkboxOpt; +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index c3d0dce70..068358ade 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -6,8 +6,7 @@ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString - SELECTED = 259, // bool - INSTALLED = 260 // bool + INSTALLED = 259 // bool }; /** This is an item delegate composed of: @@ -22,4 +21,12 @@ class ProjectItemDelegate final : public QStyledItemDelegate { ProjectItemDelegate(QWidget* parent); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + + bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override; + + signals: + void checkboxClicked(const QModelIndex& index); + + private: + QStyleOptionViewItem makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const; }; 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 7a54bd390..097678b8d 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -87,7 +87,7 @@ void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); - auto newIconTheme = ui->iconsComboBox->currentData().toString(); + auto newIconTheme = ui->iconsComboBox->itemData(index).toString(); if (originalIconTheme != newIconTheme) { settings->set("IconTheme", newIconTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -100,7 +100,7 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) { auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); + auto newAppTheme = ui->widgetStyleComboBox->itemData(index).toString(); if (originalAppTheme != newAppTheme) { settings->set("ApplicationTheme", newAppTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); @@ -113,7 +113,7 @@ void ThemeCustomizationWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); auto originalCat = settings->get("BackgroundCat").toString(); - auto newCat = ui->backgroundCatComboBox->currentData().toString(); + auto newCat = ui->backgroundCatComboBox->itemData(index).toString(); if (originalCat != newCat) { settings->set("BackgroundCat", newCat); } diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 8bf8cb473..815924d52 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -46,31 +46,12 @@ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include -#include -#include #include -#include +#include "console/WindowsConsole.h" #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif #include "DesktopServices.h" @@ -103,112 +84,12 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } } -#if defined Q_OS_WIN32 - -// taken from https://stackoverflow.com/a/25927081 -// getting a proper output to console with redirection support on windows is apparently hell -void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) -{ - // Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been - // observed that the file number of our standard handle file objects can be assigned internally to a value of -2 - // when not bound to a valid target, which represents some kind of unknown internal invalid state. In this state our - // call to "_dup2" fails, as it specifically tests to ensure that the target file number isn't equal to this value - // before allowing the operation to continue. We can resolve this issue by first "re-opening" the target files to - // use the "nul" device, which will place them into a valid state, after which we can redirect them to our target - // using the "_dup2" function. - if (bindStdIn) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "r", stdin); - } - if (bindStdOut) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stdout); - } - if (bindStdErr) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stderr); - } - - // Redirect unbuffered stdin from the current standard input handle - if (bindStdIn) { - HANDLE stdHandle = GetStdHandle(STD_INPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "r"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdin)); - if (dup2Result == 0) { - setvbuf(stdin, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stdout to the current standard output handle - if (bindStdOut) { - HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdout)); - if (dup2Result == 0) { - setvbuf(stdout, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stderr to the current standard error handle - if (bindStdErr) { - HANDLE stdHandle = GetStdHandle(STD_ERROR_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stderr)); - if (dup2Result == 0) { - setvbuf(stderr, NULL, _IONBF, 0); - } - } - } - } - } - - // Clear the error state for each of the C++ standard stream objects. We need to do this, as attempts to access the - // standard streams before they refer to a valid target will cause the iostream objects to enter an error state. In - // versions of Visual Studio after 2005, this seems to always occur during startup regardless of whether anything - // has been read from or written to the targets or not. - if (bindStdIn) { - std::wcin.clear(); - std::cin.clear(); - } - if (bindStdOut) { - std::wcout.clear(); - std::cout.clear(); - } - if (bindStdErr) { - std::wcerr.clear(); - std::cerr.clear(); - } -} -#endif - PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured - auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); - if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) { - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - BindCrtHandlesToStdHandles(true, true, true); - consoleAttached = true; - } + if (AttachWindowsConsole()) { + consoleAttached = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); @@ -298,6 +179,10 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar auto version_parts = version.split('.'); m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; } m_allowPreRelease = parser.isSet("pre-release"); @@ -556,6 +441,7 @@ void PrismUpdaterApp::run() m_prismVersion = BuildConfig.printableVersionString(); m_prismVersionMajor = BuildConfig.VERSION_MAJOR; m_prismVersionMinor = BuildConfig.VERSION_MINOR; + m_prismVersionPatch = BuildConfig.VERSION_PATCH; m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; m_prismGitCommit = BuildConfig.GIT_COMMIT; } @@ -564,6 +450,7 @@ void PrismUpdaterApp::run() qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; qDebug() << "Version major:" << m_prismVersionMajor; qDebug() << "Version minor:" << m_prismVersionMinor; + qDebug() << "Version minor:" << m_prismVersionPatch; qDebug() << "Version channel:" << m_prsimVersionChannel; qDebug() << "Git Commit:" << m_prismGitCommit; @@ -834,8 +721,8 @@ QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRel for_platform = false; } - auto qt_pattern = QRegularExpression("-qt(\\d+)"); - auto qt_match = qt_pattern.match(asset_name); + static const QRegularExpression s_qtPattern("-qt(\\d+)"); + auto qt_match = s_qtPattern.match(asset_name); if (for_platform && qt_match.hasMatch()) { if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" @@ -1131,12 +1018,11 @@ void PrismUpdaterApp::backupAppDir() 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 "))); + static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); auto app_dir = QDir(m_rootPath); - auto backup_dir = FS::PathCombine( - app_dir.absolutePath(), - QStringLiteral("backup_") + - QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) + "-" + - m_prismGitCommit); + auto backup_dir = + FS::PathCombine(app_dir.absolutePath(), + QStringLiteral("backup_") + QString(m_prismVersion).replace(s_replaceRegex, QString("_")) + "-" + m_prismGitCommit); FS::ensureFolderPathExists(backup_dir); auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); FS::write(backup_marker_path, backup_dir.toUtf8()); @@ -1277,6 +1163,10 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) return false; m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; m_prismGitCommit = lines.takeFirst().simplified(); return true; } @@ -1400,7 +1290,7 @@ GitHubRelease PrismUpdaterApp::getLatestRelease() bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) { - auto current_ver = Version(QString("%1.%2").arg(QString::number(m_prismVersionMajor)).arg(QString::number(m_prismVersionMinor))); + auto current_ver = Version(QString("%1.%2.%3").arg(m_prismVersionMajor).arg(m_prismVersionMinor).arg(m_prismVersionPatch)); return current_ver < release.version; } diff --git a/launcher/updater/prismupdater/PrismUpdater.h b/launcher/updater/prismupdater/PrismUpdater.h index f3dd6e062..a904cbb6f 100644 --- a/launcher/updater/prismupdater/PrismUpdater.h +++ b/launcher/updater/prismupdater/PrismUpdater.h @@ -121,6 +121,7 @@ class PrismUpdaterApp : public QApplication { QString m_prismVersion; int m_prismVersionMajor = -1; int m_prismVersionMinor = -1; + int m_prismVersionPatch = -1; QString m_prsimVersionChannel; QString m_prismGitCommit; diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp index 06dc161b1..eab3e6bbb 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -24,6 +24,7 @@ #include "ui_SelectReleaseDialog.h" +#include #include #include "Markdown.h" #include "StringUtils.h" @@ -55,6 +56,9 @@ SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const Q connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SelectReleaseDialog::~SelectReleaseDialog() diff --git a/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt index b736cefcb..dd78647c0 100644 --- a/libraries/LocalPeer/CMakeLists.txt +++ b/libraries/LocalPeer/CMakeLists.txt @@ -1,9 +1,7 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(LocalPeer) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Network REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Network Core5Compat REQUIRED) list(APPEND LocalPeer_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index bd407042f..cb74c031b 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -72,11 +72,12 @@ ApplicationId ApplicationId::fromTraditionalApp() protoId = protoId.toLower(); #endif auto prefix = protoId.section(QLatin1Char('/'), -1); - prefix.remove(QRegularExpression("[^a-zA-Z]")); + static const QRegularExpression s_removeChars("[^a-zA-Z]"); + prefix.remove(s_removeChars); prefix.truncate(6); QByteArray idc = protoId.toUtf8(); - quint16 idNum = qChecksum(idc.constData(), idc.size()); - auto socketName = QLatin1String("qtsingleapp-") + prefix + QLatin1Char('-') + QString::number(idNum, 16); + quint16 idNum = qChecksum(idc); + auto socketName = QLatin1String("pl") + prefix + QLatin1Char('-') + QString::number(idNum, 16).left(12); #if defined(Q_OS_WIN) if (!pProcessIdToSessionId) { QLibrary lib("kernel32"); @@ -98,12 +99,12 @@ ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const Q QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); QString result = dataPath + QLatin1Char('-') + version; shasum.addData(result.toUtf8()); - return ApplicationId(QLatin1String("qtsingleapp-") + QString::fromLatin1(shasum.result().toHex())); + return ApplicationId(QLatin1String("pl") + QString::fromLatin1(shasum.result().toHex()).left(12)); } ApplicationId ApplicationId::fromCustomId(const QString& id) { - return ApplicationId(QLatin1String("qtsingleapp-") + id); + return ApplicationId(QLatin1String("pl") + id); } ApplicationId ApplicationId::fromRawString(const QString& id) @@ -139,7 +140,7 @@ bool LocalPeer::isClient() #if defined(Q_OS_UNIX) // ### Workaround if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { - QFile::remove(QDir::cleanPath(QDir::tempPath()) + QLatin1Char('/') + socketName); + QLocalServer::removeServer(socketName); res = server->listen(socketName); } #endif diff --git a/libraries/LocalPeer/src/LockedFile.h b/libraries/LocalPeer/src/LockedFile.h index e8023251c..0d3539708 100644 --- a/libraries/LocalPeer/src/LockedFile.h +++ b/libraries/LocalPeer/src/LockedFile.h @@ -42,7 +42,7 @@ #include #ifdef Q_OS_WIN -#include +#include #endif class LockedFile : public QFile { @@ -64,7 +64,7 @@ class LockedFile : public QFile { #ifdef Q_OS_WIN Qt::HANDLE wmutex; Qt::HANDLE rmutex; - QVector rmutexes; + QList rmutexes; QString mutexname; Qt::HANDLE getMutexHandle(int idx, bool doCreate); diff --git a/libraries/README.md b/libraries/README.md index 67d78dade..5f7b685e5 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -2,14 +2,6 @@ This folder has third-party or otherwise external libraries needed for other parts to work. -## filesystem - -Gulrak's implementation of C++17 std::filesystem for C++11 /C++14/C++17/C++20 on Windows, macOS, Linux and FreeBSD. - -See [github repo](https://github.com/gulrak/filesystem). - -MIT licensed. - ## gamemode A performance optimization daemon. @@ -107,6 +99,14 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). +## qt-qrcodegenerator + +A simple library for generating QR codes + +See [github repo](https://github.com/nayuki/QR-Code-generator). + +MIT + ## quazip A zip manipulation library. diff --git a/libraries/cmark b/libraries/cmark index 8fbf02968..3460cd809 160000 --- a/libraries/cmark +++ b/libraries/cmark @@ -1 +1 @@ -Subproject commit 8fbf029685482827828b5858444157052f1b0a5f +Subproject commit 3460cd809b6dd311b58e92733ece2fc956224fd2 diff --git a/libraries/extra-cmake-modules b/libraries/extra-cmake-modules index bbcbaff78..1f820dc98 160000 --- a/libraries/extra-cmake-modules +++ b/libraries/extra-cmake-modules @@ -1 +1 @@ -Subproject commit bbcbaff78283270c2beee69afd8d5b91da854af8 +Subproject commit 1f820dc98d0a520c175433bcbb0098327d82aac6 diff --git a/libraries/filesystem b/libraries/filesystem deleted file mode 160000 index 2fc4b4637..000000000 --- a/libraries/filesystem +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2fc4b463759e043476fc0036da094e5877e3dd50 diff --git a/libraries/gamemode/CMakeLists.txt b/libraries/gamemode/CMakeLists.txt index 9e07f34ac..61195ac21 100644 --- a/libraries/gamemode/CMakeLists.txt +++ b/libraries/gamemode/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(gamemode VERSION 1.6.1) diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index fd545d2bc..b9bcb121a 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 4cd1ba58b..dfc4ebb32 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index dc518be64..968499ff6 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -76,13 +76,10 @@ public final class StandardLauncher extends AbstractLauncher { @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) { if (quickPlayMultiplayerSupported) { @@ -100,6 +97,19 @@ public final class StandardLauncher extends AbstractLauncher { gameArgs.add(worldName); } + StringBuilder joinedGameArgs = new StringBuilder(); + for (String gameArg : gameArgs) { + if (joinedGameArgs.length() > 0) { + joinedGameArgs.append('\u001F'); // unit separator, designed for this purpose + } + joinedGameArgs.append(gameArg); + } + + // pass the real main class and game arguments in so mods can access them + System.setProperty("org.prismlauncher.launch.mainclass", mainClassName); + // unit separator ('\u001F') delimited list of game args + System.setProperty("org.prismlauncher.launch.gameargs", joinedGameArgs.toString()); + // find and invoke the main method MethodHandle method = ReflectionUtils.findMainMethod(mainClassName); method.invokeExact(gameArgs.toArray(new String[0])); diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index 23b955121..531449ba1 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit 23b955121b8217c1c348a9ed2483167a6f3ff4ad +Subproject commit 531449ba1c930c98e0bcf5d332b237a8566f9d78 diff --git a/libraries/murmur2/CMakeLists.txt b/libraries/murmur2/CMakeLists.txt index f3068201d..be989ee36 100644 --- a/libraries/murmur2/CMakeLists.txt +++ b/libraries/murmur2/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(murmur2) set(MURMUR_SOURCES diff --git a/libraries/qdcss/CMakeLists.txt b/libraries/qdcss/CMakeLists.txt index 0afdef321..7e497feca 100644 --- a/libraries/qdcss/CMakeLists.txt +++ b/libraries/qdcss/CMakeLists.txt @@ -1,9 +1,7 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(qdcss) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED) list(APPEND qdcss_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() diff --git a/libraries/qdcss/src/qdcss.cpp b/libraries/qdcss/src/qdcss.cpp index c531fb63d..bf0ef63cb 100644 --- a/libraries/qdcss/src/qdcss.cpp +++ b/libraries/qdcss/src/qdcss.cpp @@ -8,19 +8,19 @@ #include #include -QRegularExpression ruleset_re = QRegularExpression(R"([#.]?(@?\w+?)\s*\{(.*?)\})", QRegularExpression::DotMatchesEverythingOption); -QRegularExpression rule_re = QRegularExpression(R"((\S+?)\s*:\s*(?:\"(.*?)(? 0 ? qr.getSize() : 1; + const double w = sz.width(); + const double h = sz.height(); + const double aspect = w / h; + const double size = ((aspect > 1.0) ? h : w); + const double scale = size / (s + 2); + // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color. + // It expects background to be prepared already (in white or whatever is preferred). + painter.setPen(Qt::NoPen); + painter.setBrush(fg); + for (int y = 0; y < s; y++) { + for (int x = 0; x < s; x++) { + const int color = qr.getModule(x, y); // 0 for white, 1 for black + if (0 != color) { + const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; + QRectF r(rx1, ry1, scale, scale); + painter.drawRects(&r, 1); + } + } + } +} \ No newline at end of file diff --git a/libraries/qt-qrcodegenerator/qr.h b/libraries/qt-qrcodegenerator/qr.h new file mode 100644 index 000000000..290d49001 --- /dev/null +++ b/libraries/qt-qrcodegenerator/qr.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include +#include + +// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c +void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg); diff --git a/libraries/quazip b/libraries/quazip index 9d3aa3ee9..3fd3b299b 160000 --- a/libraries/quazip +++ b/libraries/quazip @@ -1 +1 @@ -Subproject commit 9d3aa3ee948c1cde5a9f873ecbc3bb229c1182ee +Subproject commit 3fd3b299b875fbd2beac4894b8a870d80022cad7 diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index b6bbe7101..c971889d8 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -1,9 +1,7 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(rainbow) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Gui REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Gui REQUIRED) endif() diff --git a/libraries/systeminfo/CMakeLists.txt b/libraries/systeminfo/CMakeLists.txt index 33d246050..80b6b8094 100644 --- a/libraries/systeminfo/CMakeLists.txt +++ b/libraries/systeminfo/CMakeLists.txt @@ -1,8 +1,6 @@ project(systeminfo) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED) list(APPEND systeminfo_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() diff --git a/libraries/systeminfo/src/distroutils.cpp b/libraries/systeminfo/src/distroutils.cpp index 57e6c8320..294dfadbb 100644 --- a/libraries/systeminfo/src/distroutils.cpp +++ b/libraries/systeminfo/src/distroutils.cpp @@ -40,6 +40,8 @@ SOFTWARE. #include +static const QRegularExpression s_distoSplitRegex("\\s+"); + Sys::DistributionInfo Sys::read_os_release() { Sys::DistributionInfo out; @@ -145,11 +147,7 @@ void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) vers = lsb.codename; } else { // ubuntu, debian, gentoo, scientific, slackware, ... ? -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto parts = dist.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - auto parts = dist.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + auto parts = dist.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (parts.size()) { dist = parts[0]; } @@ -182,11 +180,7 @@ QString Sys::_extract_distribution(const QString& x) if (release.startsWith("suse linux enterprise")) { return "sles"; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList list = release.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = release.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + QStringList list = release.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (list.size()) { return list[0]; } @@ -195,15 +189,11 @@ QString Sys::_extract_distribution(const QString& x) QString Sys::_extract_version(const QString& x) { - QRegularExpression versionish_string(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList list = x.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = x.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + static const QRegularExpression s_versionishString(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); + QStringList list = x.split(s_distoSplitRegex, Qt::SkipEmptyParts); for (int i = list.size() - 1; i >= 0; --i) { QString chunk = list[i]; - if (versionish_string.match(chunk).hasMatch()) { + if (s_versionishString.match(chunk).hasMatch()) { return chunk; } } 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 8bb658477..21714d64a 100644 --- a/nix/README.md +++ b/nix/README.md @@ -4,7 +4,7 @@ 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) @@ -44,9 +44,6 @@ Example: # 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 = ""; }; }; @@ -92,9 +89,6 @@ Example: # 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 = ""; }; }; @@ -223,4 +217,3 @@ The following parameters can be overridden: - `jdks` (default: `[ jdk21 jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable - `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication - `textToSpeechSupport` (default: `isLinux`) Turn on/off support for text-to-speech on Linux (macOS will always have this) -- `withWaylandGLFW` (default: `isLinux`) Build with support for native Wayland via a custom GLFW diff --git a/nix/checks.nix b/nix/checks.nix deleted file mode 100644 index ec219d6f8..000000000 --- a/nix/checks.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - 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/unwrapped.nix b/nix/unwrapped.nix index 7fc383ff3..b5b02b101 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -3,49 +3,69 @@ stdenv, cmake, cmark, - darwin, + apple-sdk_11, extra-cmake-modules, gamemode, - ghc_filesystem, jdk17, kdePackages, + libnbtplusplus, + qt-qrcodegenerator, ninja, - nix-filter, self, stripJavaArchivesHook, tomlplusplus, zlib, msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - version, - libnbtplusplus, + gamemodeSupport ? stdenv.hostPlatform.isLinux, }: - assert lib.assertMsg ( - gamemodeSupport -> stdenv.isLinux + gamemodeSupport -> stdenv.hostPlatform.isLinux ) "gamemodeSupport is only available on Linux."; +let + date = + let + # YYYYMMDD + date' = lib.substring 0 8 self.lastModifiedDate; + year = lib.substring 0 4 date'; + month = lib.substring 4 2 date'; + date = lib.substring 6 2 date'; + in + if (self ? "lastModifiedDate") then + lib.concatStringsSep "-" [ + year + month + date + ] + else + "unknown"; +in + stdenv.mkDerivation { pname = "prismlauncher-unwrapped"; - inherit version; + version = "10.0-unstable-${date}"; - src = nix-filter.lib { - root = self; - include = [ - "buildconfig" - "cmake" - "launcher" - "libraries" - "program_info" - "tests" - ../COPYING.md + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.unions [ ../CMakeLists.txt + ../COPYING.md + + ../buildconfig + ../cmake + ../launcher + ../libraries + ../program_info + ../tests ]; }; postUnpack = '' rm -rf source/libraries/libnbtplusplus ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + + rm -rf source/libraries/qt-qrcodegenerator/QR-Code-generator + ln -s ${qt-qrcodegenerator} source/libraries/qt-qrcodegenerator/QR-Code-generator ''; nativeBuildInputs = [ @@ -59,27 +79,26 @@ stdenv.mkDerivation { buildInputs = [ cmark - ghc_filesystem kdePackages.qtbase kdePackages.qtnetworkauth kdePackages.quazip tomlplusplus zlib ] - ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ] ++ lib.optional gamemodeSupport gamemode; - hardeningEnable = lib.optionals stdenv.isLinux [ "pie" ]; + hardeningEnable = lib.optionals stdenv.hostPlatform.isLinux [ "pie" ]; cmakeFlags = - [ (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") ] + [ + # downstream branding + (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") + ] ++ lib.optionals (msaClientID != null) [ (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) ] - ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ - (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") - ] - ++ lib.optionals stdenv.isDarwin [ + ++ lib.optionals stdenv.hostPlatform.isDarwin [ # we wrap our binary manually (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") # disable built-in updater diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 5632d483b..03c1f0421 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -1,73 +1,65 @@ { - lib, - stdenv, - symlinkJoin, - prismlauncher-unwrapped, - addOpenGLRunpath, + addDriverRunpath, + alsa-lib, flite, gamemode, - glfw, - glfw-wayland-minecraft, - glxinfo, - jdk8, + glfw3-minecraft, jdk17, jdk21, + jdk8, kdePackages, + lib, libGL, + libX11, + libXcursor, + libXext, + libXrandr, + libXxf86vm, + libjack2, libpulseaudio, libusb1, - makeWrapper, + mesa-demos, openal, pciutils, + pipewire, + prismlauncher-unwrapped, + stdenv, + symlinkJoin, udev, vulkan-loader, - xorg, + xrandr, + additionalLibs ? [ ], additionalPrograms ? [ ], - controllerSupport ? stdenv.isLinux, - gamemodeSupport ? stdenv.isLinux, + controllerSupport ? stdenv.hostPlatform.isLinux, + gamemodeSupport ? stdenv.hostPlatform.isLinux, jdks ? [ jdk21 jdk17 jdk8 ], msaClientID ? null, - textToSpeechSupport ? stdenv.isLinux, - # Adds `glfw-wayland-minecraft` to `LD_LIBRARY_PATH` - # when launched on wayland, allowing for the game to be run natively. - # Make sure to enable "Use system installation of GLFW" in instance settings - # for this to take effect - # - # Warning: This build of glfw may be unstable, and the launcher - # itself can take slightly longer to start - withWaylandGLFW ? false, + textToSpeechSupport ? stdenv.hostPlatform.isLinux, }: assert lib.assertMsg ( - controllerSupport -> stdenv.isLinux + controllerSupport -> stdenv.hostPlatform.isLinux ) "controllerSupport only has an effect on Linux."; assert lib.assertMsg ( - textToSpeechSupport -> stdenv.isLinux + textToSpeechSupport -> stdenv.hostPlatform.isLinux ) "textToSpeechSupport only has an effect on Linux."; -assert lib.assertMsg ( - withWaylandGLFW -> stdenv.isLinux -) "withWaylandGLFW is only available on Linux."; - let prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; }; in + symlinkJoin { name = "prismlauncher-${prismlauncher'.version}"; paths = [ prismlauncher' ]; - nativeBuildInputs = - [ kdePackages.wrapQtAppsHook ] - # purposefully using a shell wrapper here for variable expansion - # see https://github.com/NixOS/nixpkgs/issues/172583 - ++ lib.optional withWaylandGLFW makeWrapper; + nativeBuildInputs = [ kdePackages.wrapQtAppsHook ]; buildInputs = [ @@ -75,45 +67,39 @@ symlinkJoin { kdePackages.qtsvg ] ++ lib.optional ( - lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.isLinux + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux ) kdePackages.qtwayland; - env = { - waylandPreExec = lib.optionalString withWaylandGLFW '' - if [ -n "$WAYLAND_DISPLAY" ]; then - export LD_LIBRARY_PATH=${lib.getLib glfw-wayland-minecraft}/lib:"$LD_LIBRARY_PATH" - fi - ''; - }; - - postBuild = - lib.optionalString withWaylandGLFW '' - qtWrapperArgs+=(--run "$waylandPreExec") - '' - + '' - wrapQtAppsHook - ''; + postBuild = '' + wrapQtAppsHook + ''; qtWrapperArgs = let runtimeLibs = [ - # lwjgl - glfw - libpulseaudio - libGL + (lib.getLib stdenv.cc.cc) + ## native versions + glfw3-minecraft openal - stdenv.cc.cc.lib - vulkan-loader # VulkanMod's lwjgl + ## openal + alsa-lib + libjack2 + libpulseaudio + pipewire + + ## glfw + libGL + libX11 + libXcursor + libXext + libXrandr + libXxf86vm udev # oshi - xorg.libX11 - xorg.libXext - xorg.libXcursor - xorg.libXrandr - xorg.libXxf86vm + vulkan-loader # VulkanMod's lwjgl ] ++ lib.optional textToSpeechSupport flite ++ lib.optional gamemodeSupport gamemode.lib @@ -121,14 +107,15 @@ symlinkJoin { ++ additionalLibs; runtimePrograms = [ - glxinfo + mesa-demos pciutils # need lspci - xorg.xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 ] ++ additionalPrograms; + in [ "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] - ++ lib.optionals stdenv.isLinux [ - "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" + ++ lib.optionals stdenv.hostPlatform.isLinux [ + "--set LD_LIBRARY_PATH ${addDriverRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" "--prefix PATH : ${lib.makeBinPath runtimePrograms}" ]; diff --git a/program_info/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 e693d757a..db6920e20 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -14,20 +14,22 @@ set(Launcher_DisplayName "Prism Launcher") set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE) set(Launcher_DisplayName "${Launcher_DisplayName}" 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_AppID "org.prismlauncher.PrismLauncher") +set(Launcher_SVGFileName "${Launcher_AppID}.svg") +set(Launcher_Copyright "© 2022-2025 Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") +set(Launcher_Copyright_Mac "© 2022-2025 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) set(Launcher_ConfigFile "prismlauncher.cfg" PARENT_SCOPE) set(Launcher_Git "https://github.com/PrismLauncher/PrismLauncher" PARENT_SCOPE) -set(Launcher_DesktopFileName "org.prismlauncher.PrismLauncher.desktop" PARENT_SCOPE) -set(Launcher_SVGFileName "org.prismlauncher.PrismLauncher.svg" PARENT_SCOPE) +set(Launcher_AppID "${Launcher_AppID}" PARENT_SCOPE) +set(Launcher_SVGFileName "${Launcher_SVGFileName}" PARENT_SCOPE) -set(Launcher_Desktop "program_info/org.prismlauncher.PrismLauncher.desktop" PARENT_SCOPE) +set(Launcher_Desktop "program_info/${Launcher_AppID}.desktop" PARENT_SCOPE) set(Launcher_mrpack_MIMEInfo "program_info/modrinth-mrpack-mime.xml" PARENT_SCOPE) -set(Launcher_MetaInfo "program_info/org.prismlauncher.PrismLauncher.metainfo.xml" PARENT_SCOPE) -set(Launcher_SVG "program_info/org.prismlauncher.PrismLauncher.svg" PARENT_SCOPE) +set(Launcher_MetaInfo "program_info/${Launcher_AppID}.metainfo.xml" PARENT_SCOPE) +set(Launcher_SVG "program_info/${Launcher_SVGFileName}" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" PARENT_SCOPE) set(Launcher_Branding_ICO "program_info/prismlauncher.ico") set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) @@ -36,11 +38,13 @@ set(Launcher_Branding_LogoQRC "program_info/prismlauncher.qrc" PARENT_SCOPE) set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) -configure_file(org.prismlauncher.PrismLauncher.desktop.in org.prismlauncher.PrismLauncher.desktop) -configure_file(org.prismlauncher.PrismLauncher.metainfo.xml.in org.prismlauncher.PrismLauncher.metainfo.xml) +configure_file(${Launcher_AppID}.desktop.in ${Launcher_AppID}.desktop) +configure_file(${Launcher_AppID}.metainfo.xml.in ${Launcher_AppID}.metainfo.xml) configure_file(prismlauncher.rc.in prismlauncher.rc @ONLY) +configure_file(prismlauncher.qrc.in prismlauncher.qrc @ONLY) configure_file(prismlauncher.manifest.in prismlauncher.manifest @ONLY) configure_file(prismlauncher.ico prismlauncher.ico COPYONLY) +configure_file(${Launcher_SVGFileName} ${Launcher_SVGFileName} COPYONLY) if(MSVC) set(Launcher_MSVC_Redist_NSIS_Section [=[ diff --git a/program_info/genicons.sh b/program_info/genicons.sh index fe8d2e35e..b62cf4f16 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -1,5 +1,7 @@ #!/bin/bash +LAUNCHER_APPID="org.prismlauncher.PrismLauncher" + svg2png() { input_file="$1" output_file="$2" @@ -9,26 +11,19 @@ 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) - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_16.png" 16 16 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_24.png" 24 24 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_32.png" 32 32 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_48.png" 48 48 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_64.png" 64 64 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_128.png" 128 128 - svg2png org.prismlauncher.PrismLauncher.svg "$d/prismlauncher_256.png" 256 256 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_16.png" 16 16 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_24.png" 24 24 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_32.png" 32 32 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_48.png" 48 48 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_64.png" 64 64 + svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_128.png" 128 128 + svg2png ${LAUNCHER_APPID}.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" \ @@ -40,10 +35,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,20 +46,23 @@ if command -v "inkscape" && command -v "sips" && command -v "iconutil"; then mkdir -p "$d" - 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 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16.png" 16 16 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16@2.png" 32 32 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32.png" 32 32 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32@2.png" 64 64 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128.png" 128 128 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128@2.png" 256 256 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256.png" 256 256 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256@2.png" 512 512 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_512x512@2x.png" 1024 1024 + + 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 -cp -v org.prismlauncher.PrismLauncher.svg "../launcher/resources/multimc/scalable/launcher.svg" +cp -v ${LAUNCHER_APPID}.svg "../launcher/resources/multimc/scalable/launcher.svg" diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index c0e4e9458..182d02b1d 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -7,7 +7,7 @@ Terminal=false Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true Icon=org.@Launcher_APP_BINARY_NAME@.@Launcher_CommonName@ -Categories=Game;ActionGame;AdventureGame;Simulation; +Categories=Game;ActionGame;AdventureGame;Simulation;PackageManager; Keywords=game;minecraft;mc; 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..95bb86a27 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -1,10 +1,10 @@ - org.prismlauncher.PrismLauncher - org.prismlauncher.PrismLauncher.desktop + @Launcher_AppID@ + @Launcher_AppID@.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.qrc b/program_info/prismlauncher.qrc.in similarity index 60% rename from program_info/prismlauncher.qrc rename to program_info/prismlauncher.qrc.in index 4f326c2bc..d1e1cdd13 100644 --- a/program_info/prismlauncher.qrc +++ b/program_info/prismlauncher.qrc.in @@ -1,6 +1,6 @@ - org.prismlauncher.PrismLauncher.svg + @Launcher_AppID@.svg diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 24f6ee4e8..dfdce2e1c 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -398,6 +398,7 @@ Section "@Launcher_DisplayName@" WriteRegStr HKCU Software\Classes\prismlauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' ; Write the uninstall keys for Windows + ; https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} 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/shell.nix b/shell.nix new file mode 100644 index 000000000..21bab1009 --- /dev/null +++ b/shell.nix @@ -0,0 +1,4 @@ +(import (fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; + sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; +}) { src = ./.; }).shellNix diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2dedb47cc..31b887ff1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,3 +62,7 @@ ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VE ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) + + +ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME XmlLogs) diff --git a/tests/DataPackParse_test.cpp b/tests/DataPackParse_test.cpp index cd6ae8e8f..14f80858f 100644 --- a/tests/DataPackParse_test.cpp +++ b/tests/DataPackParse_test.cpp @@ -38,7 +38,7 @@ class DataPackParseTest : public QObject { QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip"); DataPack pack{ QFileInfo(zip_dp) }; - bool valid = DataPackUtils::processZIP(pack); + bool valid = DataPackUtils::processZIP(&pack); QVERIFY(pack.packFormat() == 4); QVERIFY(pack.description() == "Some data pack 2 boobgaloo"); @@ -52,7 +52,7 @@ class DataPackParseTest : public QObject { QString folder_dp = FS::PathCombine(source, "test_folder"); DataPack pack{ QFileInfo(folder_dp) }; - bool valid = DataPackUtils::processFolder(pack); + bool valid = DataPackUtils::processFolder(&pack); QVERIFY(pack.packFormat() == 10); QVERIFY(pack.description() == "Some data pack, maybe"); @@ -66,7 +66,7 @@ class DataPackParseTest : public QObject { QString folder_dp = FS::PathCombine(source, "another_test_folder"); DataPack pack{ QFileInfo(folder_dp) }; - bool valid = DataPackUtils::process(pack); + bool valid = DataPackUtils::process(&pack); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "Some data pack three, leaves on the tree"); 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..995867e46 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -2,30 +2,15 @@ #include #include #include +#include #include #include #include -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif #include @@ -42,7 +27,7 @@ class LinkTask : public Task { ~LinkTask() { delete m_lnk; } - void matcher(const IPathMatcher* filter) { m_lnk->matcher(filter); } + void matcher(IPathMatcher::Ptr filter) { m_lnk->matcher(filter); } void linkRecursively(bool recursive) { @@ -63,7 +48,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 +98,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 +123,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 } @@ -237,8 +188,8 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - c.matcher(&re); + RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + c.matcher(re); c(); for (auto entry : target_dir.entryList()) { @@ -270,8 +221,8 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - c.matcher(&re); + RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + c.matcher(re); c.whitelist(true); c(); @@ -369,11 +320,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; @@ -462,14 +414,15 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - lnk_tsk.matcher(&re); + RegexpMatcher::Ptr re = std::make_shared("[.]?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; @@ -508,15 +461,16 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - lnk_tsk.matcher(&re); + RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + 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 +510,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 +559,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 +595,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 +646,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/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp index 350ab615e..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,7 +199,7 @@ 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); @@ -210,7 +210,7 @@ class ResourceFolderModelTest : public QObject { 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/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index b589758aa..30bb99fb8 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -43,8 +43,8 @@ class DummyResourceModel : public ResourceModel { [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; } ResourceAPI::SearchArgs createSearchArguments() override { return {}; } - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; } - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; } + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override { return {}; } + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override { return {}; } QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index e1092167d..17c0482fc 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -18,6 +18,7 @@ #include #include +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include @@ -35,7 +36,7 @@ class ResourcePackParseTest : public QObject { QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack{ QFileInfo(zip_rp) }; - bool valid = ResourcePackUtils::processZIP(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::processZIP(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 3); QVERIFY(pack.description() == @@ -51,7 +52,7 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; - bool valid = ResourcePackUtils::processFolder(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::processFolder(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 1); QVERIFY(pack.description() == "Some resource pack maybe"); @@ -65,7 +66,7 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; - bool valid = ResourcePackUtils::process(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::process(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 0740ba0a3..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(); } @@ -66,7 +66,7 @@ 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(); }); @@ -128,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() @@ -154,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 @@ -196,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() @@ -219,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() @@ -242,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/XmlLogs_test.cpp b/tests/XmlLogs_test.cpp new file mode 100644 index 000000000..e01238570 --- /dev/null +++ b/tests/XmlLogs_test.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 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 + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +class XmlLogParseTest : public QObject { + Q_OBJECT + + private slots: + + void parseXml_data() + { + QString source = QFINDTESTDATA("testdata/TestLogs"); + + QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); + QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); + QStringList shortTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList shortTextLevels; + shortTextLevels.reserve(24); + std::transform(shortTextLevels_s.cbegin(), shortTextLevels_s.cend(), std::back_inserter(shortTextLevels), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); + + QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); + QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); + QStringList longTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + QStringList longTextLevelsXml_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-xml-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList longTextLevelsPlain; + longTextLevelsPlain.reserve(974); + std::transform(longTextLevels_s.cbegin(), longTextLevels_s.cend(), std::back_inserter(longTextLevelsPlain), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); + QList longTextLevelsXml; + longTextLevelsXml.reserve(896); + std::transform(longTextLevelsXml_s.cbegin(), longTextLevelsXml_s.cend(), std::back_inserter(longTextLevelsXml), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); + + QTest::addColumn("log"); + QTest::addColumn("num_entries"); + QTest::addColumn>("entry_levels"); + + QTest::newRow("short-vanilla-plain") << shortText << 25 << shortTextLevels; + QTest::newRow("short-vanilla-xml") << shortXml << 25 << shortTextLevels; + QTest::newRow("long-forge-plain") << longText << 945 << longTextLevelsPlain; + QTest::newRow("long-forge-xml") << longXml << 869 << longTextLevelsXml; + } + + void parseXml() + { + QFETCH(QString, log); + QFETCH(int, num_entries); + QFETCH(QList, entry_levels); + + QList> entries = {}; + + QBENCHMARK + { + entries = parseLines(log.split(QRegularExpression("\n|\r\n|\r"))); + } + + QCOMPARE(entries.length(), num_entries); + + QList levels = {}; + + std::transform(entries.cbegin(), entries.cend(), std::back_inserter(levels), + [](std::pair entry) { return entry.first; }); + + QCOMPARE(levels, entry_levels); + } + + private: + LogParser m_parser; + + QList> parseLines(const QStringList& lines) + { + QList> out; + MessageLevel::Enum last = MessageLevel::Unknown; + + for (const auto& line : lines) { + m_parser.appendLine(line); + + auto items = m_parser.parseAvailable(); + for (const auto& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + out.append(std::make_pair(entry.level, msg)); + last = entry.level; + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + auto level = LogParser::guessLevel(msg, last); + out.append(std::make_pair(level, msg)); + last = level; + } + } + } + return out; + } +}; + +QTEST_GUILESS_MAIN(XmlLogParseTest) + +#include "XmlLogs_test.moc" diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log new file mode 100644 index 000000000..c0775ebb7 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log @@ -0,0 +1,947 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG +2025-04-18 12:47:23,932 main WARN Advanced terminal features are not available in this environment +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher running: args [--username, Ryexandrite, --version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --assetIndex, 5, --uuid, , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480] +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher 10.0.9+10.0.9+main.dcd20f30 starting: java version 17.0.8 by Microsoft; OS Linux arch amd64 version 6.6.85 +[12:47:24] [main/INFO] [ne.mi.fm.lo.ImmediateWindowHandler/]: Loading ImmediateWindowProvider fmlearlywindow +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Trying GL version 4.6 +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Requested GL version 4.6 got version 4.6 +[12:47:24] [main/INFO] [mixin/]: SpongePowered MIXIN Subsystem Version=0.8.5 Source=union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar%23140!/ Service=ModLauncher Env=CLIENT +[12:47:24] [pool-2-thread-1/INFO] [EARLYDISPLAY/]: GL info: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) GL version 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f), AMD +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/fmlcore/1.20.1-47.2.6/fmlcore-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/javafmllanguage/1.20.1-47.2.6/javafmllanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/lowcodelanguage/1.20.1-47.2.6/lowcodelanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/mclanguage/1.20.1-47.2.6/mclanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.ja.se.JarSelector/]: Attempted to select two dependency jars from JarJar which have the same identification: Mod File: and Mod File: . Using Mod File: +[12:47:25] [main/INFO] [ne.mi.fm.lo.mo.JarInJarDependencyLocator/]: Found 28 dependencies adding them to mods collection +[12:47:28] [main/ERROR] [mixin/]: Mixin config dynamiclightsreforged.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [mixin/]: Compatibility level set to JAVA_17 +[12:47:28] [main/ERROR] [mixin/]: Mixin config mixins.satin.client.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config firstperson.mixins.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config yacl.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [cp.mo.mo.LaunchServiceHandler/MODLAUNCHER]: Launching target 'forgeclient' with arguments [--version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --uuid, , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480] +[12:47:28] [main/INFO] [co.ab.sa.co.Saturn/]: Loaded Saturn config file with 4 configurable options +[12:47:28] [main/INFO] [ModernFix/]: Loaded configuration file for ModernFix 5.18.1+mc1.20.1: 83 options available, 1 override(s) found +[12:47:28] [main/WARN] [ModernFix/]: Option 'mixin.perf.thread_priorities' overriden (by mods [smoothboot]) to 'false' +[12:47:28] [main/INFO] [ModernFix/]: Applying Nashorn fix +[12:47:28] [main/INFO] [ModernFix/]: Applied Forge config corruption patch +[12:47:28] [main/INFO] [fpsreducer/]: OptiFine was NOT detected. +[12:47:28] [main/INFO] [fpsreducer/]: OptiFabric was NOT detected. +[12:47:28] [main/WARN] [EmbeddiumConfig/]: Mod 'tfc' attempted to override option 'mixin.features.fast_biome_colors', which doesn't exist, ignoring +[12:47:28] [main/INFO] [Embeddium/]: Loaded configuration file for Embeddium: 205 options available, 3 override(s) found +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Searching for graphics cards... +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Found graphics card: GraphicsAdapterInfo[vendor=AMD, name=Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT], version=unknown] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: Sodium has applied one or more workarounds to prevent crashes or other issues on your system: [NO_ERROR_CONTEXT_UNSUPPORTED] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: This is not necessarily an issue, but it may result in certain features or optimizations being disabled. You can sometimes fix these issues by upgrading your graphics driver. +[12:47:28] [main/INFO] [Radium Config/]: Loaded configuration file for Radium: 125 options available, 7 override(s) found +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-common-refmap.json' for carpeted-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-forge-refmap.json' for carpeted.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'emi-forge-refmap.json' for emi-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-common-refmap.json' for ftbfiltersystem-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-forge-refmap.json' for ftbfiltersystem.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/INFO] [Puzzles Lib/]: Loading 160 mods: + - additionalplacements 1.8.0 + - ae2 15.2.13 + - ae2insertexportcard 1.20.1-1.3.0 + - ae2netanalyser 1.20-1.0.6-forge + - ae2wtlib 15.2.3-forge + - aiimprovements 0.5.2 + - ambientsounds 6.1.1 + - architectury 9.2.14 + - astikorcarts 1.1.8 + - attributefix 21.0.4 + - balm 7.3.9 + \-- kuma_api 20.1.8 + - barrels_2012 2.1 + - betterf3 7.0.2 + - betterfoliage 5.0.2 + - betterpingdisplay 1.1 + - betterthirdperson 1.9.0 + - blur 3.1.1 + \-- satin 1.20.1+1.15.0-SNAPSHOT + - carpeted 1.20-1.4 + - carryon 2.1.2.7 + \-- mixinextras 0.2.0-beta.6 + - catalogue 1.8.0 + - chat_heads 0.13.9 + - cherishedworlds 6.1.7+1.20.1 + - clienttweaks 11.1.0 + - cloth_config 11.1.136 + - clumps 12.0.0.4 + - computercraft 1.113.1 + - controlling 12.0.2 + - coralstfc 1.0.0 + - corpse 1.20.1-1.0.19 + - cosmeticarmorreworked 1.20.1-v1a + - craftingtweaks 18.2.5 + - craftpresence 2.5.0 + - create 0.5.1.f + \-- flywheel 0.6.10-7 + - create_connected 0.8.2-mc1.20.1 + - createaddition 1.20.1-1.2.4c + - creativecore 2.12.15 + - cucumber 7.0.12 + - cupboard 1.20.1-2.7 + - curios 5.10.0+1.20.1 + - defaultoptions 18.0.1 + - do_a_barrel_roll 3.5.6+1.20.1 + - drippyloadingscreen 3.0.1 + - dynamiclightsreforged 1.20.1_v1.6.0 + - embeddium 0.3.19+mc1.20.1 + \-- rubidium 0.7.1 + - embeddiumplus 1.2.12 + - emi 1.1.7+1.20.1+forge + - enhancedvisuals 1.8.1 + - etched 3.0.2 + - everycomp 1.20-2.7.12 + - expatternprovider 1.20-1.1.14-forge + - exposure 1.7.7 + - fallingtrees 0.12.7 + - fancymenu 3.2.3 + - ferritecore 6.0.1 + - firmaciv 0.2.10-alpha-1.20.1 + - firmalife 2.1.15 + - firstperson 2.4.5 + - flickerfix 4.0.1 + - forge 47.2.6 + - fpsreducer 1.20-2.5 + - framedblocks 9.3.1 + - ftbbackups2 1.0.23 + - ftbessentials 2001.2.2 + - ftbfiltersystem 1.0.2 + - ftblibrary 2001.2.4 + - ftbquests 2001.4.8 + - ftbranks 2001.1.3 + - ftbteams 2001.3.0 + - ftbxmodcompat 2.1.1 + - gcyr 0.1.8 + - getittogetherdrops 1.3 + - glodium 1.20-1.5-forge + - gtceu 1.2.3.a + |-- configuration 2.2.0 + \-- ldlib 1.0.25.j + - hangglider 8.0.1 + - immediatelyfast 1.2.18+1.20.4 + - inventoryhud 3.4.26 + - invtweaks 1.1.0 + - itemphysiclite 1.6.5 + - jade 11.9.4+forge + - jadeaddons 5.2.2 + - jei 15.3.0.8 + - konkrete 1.8.0 + - ksyxis 1.3.2 + - kubejs 2001.6.5-build.14 + - kubejs_create 2001.2.5-build.2 + - kubejs_tfc 1.20.1-1.1.3 + - letmedespawn 1.3.2b + - lootjs 1.20.1-2.12.0 + - megacells 2.4.4-1.20.1 + - melody 1.0.2 + - memoryleakfix 1.1.5 + - merequester 1.20.1-1.1.5 + - minecraft 1.20.1 + - modelfix 1.15 + - modernfix 5.18.1+mc1.20.1 + - moonlight 1.20-2.13.51 + - morered 4.0.0.4 + |-- jumbofurnace 4.0.0.5 + \-- useitemonblockevent 1.0.0.2 + - mousetweaks 2.25.1 + - myserveriscompatible 1.0 + - nanhealthfixer 1.20.1-0.0.1 + - nerb 0.4.1 + - noisium 2.3.0+mc1.20-1.20.1 + - noreportbutton 1.5.0 + - notenoughanimations 1.7.6 + - octolib 0.4.2 + - oculus 1.7.0 + - openpartiesandclaims 0.23.2 + - packetfixer 1.4.2 + - pandalib 0.4.2 + - patchouli 1.20.1-84-FORGE + - pickupnotifier 8.0.0 + - placebo 8.6.2 + - playerrevive 2.0.27 + - polylib 2000.0.3-build.143 + - puzzleslib 8.1.23 + \-- puzzlesaccessapi 8.0.7 + - radium 0.12.3+git.50c5c33 + - railways 1.6.4+forge-mc1.20.1 + - recipeessentials 1.20.1-3.6 + - rhino 2001.2.2-build.18 + - saturn 0.1.3 + - searchables 1.0.3 + - shimmer 1.20.1-0.2.4 + - showcaseitem 1.20.1-1.2 + - simplylight 1.20.1-1.4.6-build.50 + - smoothboot 0.0.4 + - sophisticatedbackpacks 3.20.5.1044 + - sophisticatedcore 0.6.22.611 + - supermartijn642configlib 1.1.8 + - supermartijn642corelib 1.1.17 + - tfc 3.2.12 + - tfc_tumbleweed 1.2.2 + - tfcagedalcohol 2.1 + - tfcambiental 1.20.1-3.3.0 + - tfcastikorcarts 1.1.8.2 + - tfcchannelcasting 0.2.3-beta + - tfcea 0.0.2 + - tfcgroomer 1.20.1-0.1.2 + - tfchotornot 1.0.4 + - tfcvesseltooltip 1.1 + - tfg 0.5.9 + - toofast 0.4.3.5 + - toolbelt 1.20.01 + - treetap 1.20.1-0.4.0 + - tumbleweed 0.5.5 + - unilib 1.0.2 + - uteamcore 5.1.4.312 + - waterflasks 3.0.3 + - xaerominimap 24.4.0 + - xaeroworldmap 1.39.0 + - yeetusexperimentus 2.3.1-build.6+mc1.20.1 + - yet_another_config_lib_v3 3.5.0+1.20.1-forge +[12:47:28] [main/WARN] [mixin/]: Reference map 'packetfixer-forge-forge-refmap.json' for packetfixer-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'tfchotornot.refmap.json' for tfchotornot.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/WARN] [mixin/]: Error loading class: mezz/modnametooltip/TooltipEventHandler (java.lang.ClassNotFoundException: mezz.modnametooltip.TooltipEventHandler) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/ClientHelperImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.ClientHelperImpl) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl) +[12:47:29] [main/INFO] [co.cu.Cupboard/]: Loaded config for: recipeessentials.json +[12:47:30] [main/WARN] [mixin/]: Error loading class: loaderCommon/forge/com/seibel/distanthorizons/common/wrappers/worldGeneration/mimicObject/ChunkLoader (java.lang.ClassNotFoundException: loaderCommon.forge.com.seibel.distanthorizons.common.wrappers.worldGeneration.mimicObject.ChunkLoader) +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.RenderSystemMixin will be applied. +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.WindowMixin will NOT be applied because OptiFine was NOT detected. +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin2 false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ClientPacketListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.CommandSuggestionSuggestionsListMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ConnectionMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.DownloadedPackSourceMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.FontStringRenderOutputMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageLineMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.HttpTextureMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.PlayerChatMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.SkinManagerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.compat.EmojifulMixin true false +[12:47:30] [main/WARN] [mixin/]: Error loading class: dan200/computercraft/shared/integration/jei/JEIComputerCraft (java.lang.ClassNotFoundException: dan200.computercraft.shared.integration.jei.JEIComputerCraft) +[12:47:30] [main/WARN] [mixin/]: @Mixin target dan200.computercraft.shared.integration.jei.JEIComputerCraft was not found tfg.mixins.json:common.cc.JEIComputerCraftMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/slab/CopycatSlabBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/board/CopycatBoardBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: me/jellysquid/mods/lithium/common/ai/pathing/PathNodeDefaults (java.lang.ClassNotFoundException: me.jellysquid.mods.lithium.common.ai.pathing.PathNodeDefaults) +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'alloc.blockstate.StateMixin' as option 'mixin.alloc.blockstate' (added by mods [ferritecore]) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.fluid.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.intersection.WorldMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.movement.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.AbstractMinecartEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.BoatEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityPredicatesMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityTrackingSectionMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.LivingEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'world.player_chunk_tick.ThreadedAnvilChunkStorageMixin' as option 'mixin.world.player_chunk_tick' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [mixin/]: Error loading class: org/cyclops/integrateddynamics/block/BlockCable (java.lang.ClassNotFoundException: org.cyclops.integrateddynamics.block.BlockCable) +[12:47:30] [main/WARN] [mixin/]: @Mixin target org.cyclops.integrateddynamics.block.BlockCable was not found mixins.epp.json:MixinBlockCable +[12:47:30] [main/WARN] [mixin/]: Error loading class: blusunrize/immersiveengineering/api/wires/GlobalWireNetwork (java.lang.ClassNotFoundException: blusunrize.immersiveengineering.api.wires.GlobalWireNetwork) +[12:47:30] [main/WARN] [mixin/]: @Mixin target blusunrize.immersiveengineering.api.wires.GlobalWireNetwork was not found mixins.epp.json:MixinGlobalWireNetwork +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:TornadoHelperMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:client.TornadoHelperMixin +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Will be applying 3 memory leak fixes! +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Currently enabled memory leak fixes: [targetEntityLeak, biomeTemperatureLeak, hugeScreenshotLeak] +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.WorldRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.ClientWorldMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.BackgroundRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.GlyphRendererMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.FontSetMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.shadows.EntityRenderDispatcherMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.ModelPartMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.CuboidMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.cull.EntityRendererMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [mixin/]: Error loading class: org/jetbrains/annotations/ApiStatus$Internal (java.lang.ClassNotFoundException: org.jetbrains.annotations.ApiStatus$Internal) +[12:47:31] [main/INFO] [MixinExtras|Service/]: Initializing MixinExtras via com.llamalad7.mixinextras.service.MixinExtrasServiceImpl(version=0.4.1). +[12:47:31] [main/INFO] [Smooth Boot (Reloaded)/]: Smooth Boot (Reloaded) config initialized +[12:47:31] [main/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_216202_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagOrElementLocationMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:32] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: ModernFix reached bootstrap stage (9.773 s after launch) +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByName:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByValue:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getNeighborPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getAllFlags from me.jellysquid.mods.lithium.mixin.util.block_tracking.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_6104_ in embeddium.mixins.json:features.options.render_layers.LeavesBlockMixin, previously written by me.srrapero720.embeddiumplus.mixins.impl.leaves_culling.LeavesBlockMixin. Skipping method. +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:33] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: Vanilla bootstrap took 779 milliseconds +[12:47:34] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_47505_ in lithium.mixins.json:world.temperature_cache.BiomeMixin, previously written by org.embeddedt.modernfix.common.mixin.perf.remove_biome_temperature_cache.BiomeMixin. Skipping method. +[12:47:34] [pool-4-thread-1/INFO] [co.al.me.MERequester/]: Registering content +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/assets/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/data/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/INFO] [mojang/YggdrasilAuthenticationService]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' +[12:47:35] [Render thread/INFO] [minecraft/Minecraft]: Setting user: Ryexandrite +[12:47:35] [Render thread/INFO] [ModernFix/]: Bypassed Mojang DFU +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant is searching for constants in method with descriptor (Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/GuiMessageTag;)V +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 0 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\r, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 1 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\r +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = +, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 2 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn + +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\n, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 3 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\n +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found CLASS constant: value = Ljava/lang/String;, typeValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [{}] [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 4 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [{}] [CHAT] {} +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 5 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [CHAT] {} +[12:47:35] [Render thread/INFO] [defaultoptions/]: Loaded default options for extra-folder +[12:47:35] [Render thread/INFO] [ModernFix/]: Instantiating Mojang DFU +[12:47:36] [Render thread/INFO] [minecraft/Minecraft]: Backend library: LWJGL version 3.3.1 build 7 +[12:47:36] [Render thread/INFO] [KubeJS/]: Loaded client.properties +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Vendor: AMD +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Renderer: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Version: 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/WARN] [Embeddium++/Config]: Loading Embeddium++Config +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Updating config cache +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Cache updated successfully +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Initializing ImmediatelyFast 1.2.18+1.20.4 on AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (AMD) with OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: AMD GPU detected. Enabling coherent buffer mapping +[12:47:36] [Datafixer Bootstrap/INFO] [mojang/DataFixerBuilder]: 188 Datafixer optimizations took 85 milliseconds +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Found Iris/Oculus 1.7.0. Enabling compatibility. +[12:47:36] [Render thread/INFO] [Oculus/]: Debug functionality is disabled. +[12:47:36] [Render thread/INFO] [Oculus/]: OpenGL 4.5 detected, enabling DSA. +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled because no valid shaderpack is selected +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled +[12:47:36] [modloading-worker-0/INFO] [dynamiclightsreforged/]: [LambDynLights] Initializing Dynamic Lights Reforged... +[12:47:36] [modloading-worker-0/INFO] [LowDragLib/]: LowDragLib is initializing on platform: Forge +[12:47:36] [modloading-worker-0/INFO] [in.u_.u_.ut.ve.JarSignVerifier/]: Mod uteamcore is signed with a valid certificate. +[12:47:36] [modloading-worker-0/INFO] [de.ke.me.Melody/]: [MELODY] Loading Melody background audio library.. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_109501_ in embeddium.mixins.json:core.render.world.WorldRendererMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Successfully initialized! +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Server-side libs ready to use! +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_215924_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagEntryMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: Attempting to manually load Additional Placements config early. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: manual config load successful. +[12:47:36] [modloading-worker-0/WARN] [Additional Placements/]: During block registration you may recieve several reports of "Potentially Dangerous alternative prefix `additionalplacements`". Ignore these, they are intended. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [noisium/]: Loading Noisium. +[12:47:36] [modloading-worker-0/INFO] [co.cu.Cupboard/]: Loaded config for: cupboard.json +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:container_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:tile_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:container_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_data_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_nbt +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftblibrary:edit_nbt_response +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:sync_known_server_registries +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_config +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "craftpresence" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json" +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "unilib" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json" +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbessentials:update_tab_name +[12:47:37] [modloading-worker-0/INFO] [invtweaks/]: Registered 2 network packets +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded common.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded dev.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Looking for KubeJS plugins... +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_teams +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_message_history +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: GregTechCEu is initializing on platform: Forge +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:open_gui +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Configuration settings have been saved and reloaded successfully! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:open_my_team_gui +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:update_settings +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.latvian.mods.kubejs.integration.forge.gamestages.GameStagesIntegration does not have required mod gamestages loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ldlib +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source exposure +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source tfg +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_settings_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:send_message +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ftbxmodcompat +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:send_message_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_presence +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:create_party +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:player_gui_operation +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.ftb.mods.ftbxmodcompat.ftbchunks.kubejs.FTBChunksKubeJSPlugin does not have required mod ftbchunks loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [de.ke.dr.DrippyLoadingScreen/]: [DRIPPY LOADING SCREEN] Loading v3.0.1 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source lootjs +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source cucumber +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gtceu +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Initializing TerraFirmaCraft +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Options: Assertions Enabled = false, Boostrap = false, Test = false, Debug Logging = true +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Checking Discord for available assets with Client Id: 1182610212121743470 +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Originally coded by paulhobbel - https://github.com/paulhobbel +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.ForgeMod/FORGEMOD]: Forge mod loading, version 47.2.6, for MC 1.20.1 with MCP 20230612.114412 +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.MinecraftForge/FORGE]: MinecraftForge v47.2.6 Initialized +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gcyr +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_tfc +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.precpros.PrecProsPlugin does not have required mod precisionprospecting loaded, skipping +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.afc.AFCPlugin does not have required mod afc loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_create +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. +[12:47:37] [modloading-worker-0/INFO] [Every Compat/]: Loaded EveryCompat Create Module +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gcyr config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: High-Tier is Disabled. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Using default implementation for ThreadExecutor +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Done in 309.0 ms +[12:47:37] [CraftPresence/INFO] [craftpresence/]: 3 total assets detected! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_quests +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Booting... (platform: Forge, manual: false) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Found Mixin library. (version: 0.8.5) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Ready. As always, this mod will speed up your world loading and might or might not break it. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_team_data +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.SchedulerSignalerImpl/]: Initialized Scheduler Signaller of type: class net.creeperhost.ftbbackups.repack.org.quartz.core.SchedulerSignalerImpl +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Quartz Scheduler v.2.0.2 created. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_task_progress +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.si.RAMJobStore/]: RAMJobStore initialized. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:submit_task +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler meta-data: Quartz Scheduler (v2.0.2) 'ftbbackups2' with instanceId 'NON_CLUSTERED' + Scheduler class: 'net.creeperhost.ftbbackups.repack.org.quartz.core.QuartzScheduler' - running locally. + NOT STARTED. + Currently in standby mode. + Number of jobs executed: 0 + Using thread pool 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.SimpleThreadPool' - with 1 threads. + Using job-store 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. + +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler 'ftbbackups2' initialized from an externally provided properties instance. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler version: 2.0.2 +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:claim_reward_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:get_emergency_items +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_other_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_all_rewards +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_choice_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_completion_toast +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler ftbbackups2_$_NON_CLUSTERED started. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_item_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_chapter_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_chapter_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:force_save +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:set_custom_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_lock +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:reset_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:team_data_changed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:task_screen_config_req +[12:47:37] [modloading-worker-0/INFO] [co.jo.fl.ba.Backend/]: Oculus detected. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:task_screen_config_resp +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_progress +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_task_at +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:delete_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:delete_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:edit_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:edit_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_quest_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:change_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_reward_blocking +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_chapter_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:sync_structures_request +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_structures_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:request_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editor_permission +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:open_quest_book +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:clear_display_cache +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager... +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager took 11.32 ms +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised items. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbfiltersystem:sync_filter +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised blocks. +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised block entities. +[12:47:37] [modloading-worker-0/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Loading v3.2.3 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loading config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Built config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loaded config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled KubeJS integration +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Starting... +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Loading... +[12:47:37] [modloading-worker-0/INFO] [de.to.pa.PacketFixer/]: Packet Fixer has been initialized successfully +[12:47:37] [modloading-worker-0/INFO] [YetAnotherConfigLib/]: Deserializing YACLConfig from '/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/yacl.json5' +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] All done! +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading Rhino Minecraft remapper... +[12:47:37] [modloading-worker-0/INFO] [de.la.mo.rh.mo.ut.RhinoProperties/]: Rhino properties loaded. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading mappings for 1.20.1 +[12:47:37] [modloading-worker-0/WARN] [mixin/]: @Inject(@At("INVOKE")) Shift.BY=2 on create_connected.mixins.json:sequencedgearshift.SequencedGearshiftScreenMixin::handler$cfa000$updateParamsOfRow exceeds the maximum allowed value: 0. Increase the value of maxShiftBy to suppress this warning. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Done in 0.090 s +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registered bogey styles from railways +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering data fixers +[12:47:38] [modloading-worker-0/WARN] [Railways/]: Skipping Datafixer Registration due to it being disabled in the config. +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Hex Casting +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Oh The Biomes You'll Go +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Blue Skies +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Twilight Forest +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Biomes O' Plenty +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Nature's Spirit +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Dreams and Desires +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Quark +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for TerraFirmaCraft +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:main_startup_script.js in 0.058 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfc/constants.js in 0.024 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:horornot/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:minecraft/constants.js in 0.004 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:railways/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/machines.js in 0.008 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/material_info.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/recipe_types.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:framedblocks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmalife/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:sophisticated_backpacks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:more_red/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:mega_cells/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create/constants.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ae2/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create_additions/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:chisel_and_bits/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:extended_ae2/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:asticor_carts/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ftb_quests/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/fluids.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/materials.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:computer_craft/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded 30/30 KubeJS startup scripts in 0.721 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: example.js#3: TerraFirmaGreg the best modpack in the world :) +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:example.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:tooltips.js in 0.003 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded 2/2 KubeJS client scripts in 0.022 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#48: Loaded Java class 'net.minecraft.world.level.block.AmethystClusterBlock' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#49: Loaded Java class 'net.minecraft.world.level.block.Blocks' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#50: Loaded Java class 'net.minecraft.world.level.block.state.BlockBehaviour$Properties' +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:send_data_from_client +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:send_data_from_server +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:paint +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:add_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:remove_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:sync_stages +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:first_click +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:toast +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:reload_startup_scripts +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_server_errors +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_client_errors +[12:47:38] [Render thread/INFO] [GregTechCEu/]: GTCEu common proxy init! +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering material registries +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering GTCEu Materials +[12:47:38] [CraftPresence/INFO] [craftpresence/]: Attempting to connect to Discord (1/10)... +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering addon Materials +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:water +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:lava +[12:47:39] [CraftPresence/INFO] [craftpresence/]: Loaded display data with Client Id: 1182610212121743470 (Logged in as RyRy) +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering KeyBinds +[12:47:39] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'gtceu' took 1.043 s to run a deferred task. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@67141ef8 has been registered twice for the same name ae2:export_card. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@f4864e9 has been registered twice for the same name ae2:insert_card. +[12:47:42] [Render thread/INFO] [Moonlight/]: Initialized block sets in 21ms +[12:47:42] [Render thread/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ae2wtlib:cycle_terminal +[12:47:43] [Render thread/INFO] [Every Compat/]: Registering Compat WoodType Blocks +[12:47:43] [Render thread/INFO] [Every Compat/]: EveryCompat Create Module: registered 42 WoodType blocks +[12:47:43] [Render thread/INFO] [tf.TFCTumbleweed/]: Injecting TFC Tumbleweed override pack +[12:47:43] [Render thread/INFO] [co.ee.fi.FirmaLife/]: Injecting firmalife override pack +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:43] [Render thread/INFO] [Oculus/]: Hardware information: +[12:47:43] [Render thread/INFO] [Oculus/]: CPU: 16x AMD Ryzen 7 3700X 8-Core Processor +[12:47:43] [Render thread/INFO] [Oculus/]: GPU: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (Supports OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f)) +[12:47:43] [Render thread/INFO] [Oculus/]: OS: Linux (6.6.85) +[12:47:44] [Render thread/WARN] [mixin/]: Method overwrite conflict for isHidden in mixins.oculus.compat.sodium.json:copyEntity.ModelPartMixin, previously written by dev.tr7zw.firstperson.mixins.ModelPartMixin. Skipping method. +[12:47:44] [Render thread/INFO] [minecraft/Minecraft]: [FANCYMENU] Registering resource reload listener.. +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.ScreenCustomization/]: [FANCYMENU] Initializing screen customization engine! Addons should NOT REGISTER TO REGISTRIES anymore now! +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: STARTING +[12:47:44] [Render thread/INFO] [ModernFix/]: Invalidating pack caches +[12:47:44] [Render thread/INFO] [minecraft/ReloadableResourceManager]: Reloading ResourceManager: Additional Placements blockstate redirection pack, vanilla, mod_resources, gtceu:dynamic_assets, Moonlight Mods Dynamic Assets, Firmalife-1.20.1-2.1.15.jar:overload, TFCTumbleweed-1.20.1-1.2.2.jar:overload, KubeJS Resource Pack [assets], ldlib +[12:47:44] [Finalizer/WARN] [ModernFix/]: One or more BufferBuilders have been leaked, ModernFix will attempt to correct this. +[12:47:45] [Render thread/INFO] [Every Compat/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (everycomp) in: 597 ms +[12:47:45] [Render thread/INFO] [Moonlight/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (moonlight) in: 0 ms +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for pickupnotifier +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for hangglider +[12:47:45] [Worker-ResourceReload-4/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:45] [Worker-ResourceReload-3/INFO] [xa.pa.OpenPartiesAndClaims/]: Loading Open Parties and Claims! +[12:47:45] [Worker-ResourceReload-1/INFO] [co.re.RecipeEssentials/]: recipeessentials mod initialized +[12:47:45] [Worker-ResourceReload-10/INFO] [ne.dr.tf.TerraFirmaCraft/]: TFC Common Setup +[12:47:45] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [saturn] Starting version check at https://github.com/AbdElAziz333/Saturn/raw/mc1.20.1/dev/updates.json +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting game stages provider implementation to: KubeJS Stages +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [KubeJS Stages] as the active game stages implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting permissions provider implementation to: FTB Ranks +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [FTB Ranks] as the active permissions implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] recipe helper provider is [JEI] +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled FTB Filter System integration +[12:47:45] [Render thread/INFO] [GregTechCEu/]: GregTech Model loading took 520ms +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Initializing fonts for text rendering.. +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Calculating animation sizes for FancyMenu.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: drippy_loading_overlay +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Preloading animations! This could cause the loading screen to freeze for a while.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Finished preloading animations! +[12:47:46] [Render thread/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Starting late client initialization phase.. +[12:47:46] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:46] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Starting version check at https://updates.blamejared.com/get?n=controlling&gv=1.20.1 +[12:47:46] [Worker-ResourceReload-6/ERROR] [minecraft/SimpleJsonResourceReloadListener]: Couldn't parse data file tfc:field_guide/ru_ru/entries/tfg_ores/surface_copper from tfc:patchouli_books/field_guide/ru_ru/entries/tfg_ores/surface_copper.json +com.google.gson.JsonParseException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:526) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_263475_(GsonHelper.java:531) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_13776_(GsonHelper.java:581) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_278771_(SimpleJsonResourceReloadListener.java:41) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:29) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:17) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimplePreparableReloadListener.m_10786_(SimplePreparableReloadListener.java:11) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B,pl:mixin:APP:moonlight.mixins.json:ConditionHackMixin,pl:mixin:A} + at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?] {} + at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1657) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1463) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:569) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:422) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:779) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:725) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$34$1.read(TypeAdapters.java:1007) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:524) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + ... 13 more +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Render thread/INFO] [KubeJS Client/]: Client resource reload complete! +[12:47:46] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Worker-Main-6/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Found status: BETA Current: 12.0.2 Target: 12.0.2 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Starting version check at https://api.u-team.info/update/uteamcore.json +[12:47:47] [FTB Backups Config Watcher 0/INFO] [ne.cr.ft.FTBBackups/]: Config at /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/ftbbackups2.json has changed, reloaded! +[12:47:47] [Worker-ResourceReload-14/WARN] [minecraft/SpriteLoader]: Texture create_connected:block/fluid_container_window_debug with size 40x32 limits mip level from 4 to 3 +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "unilib" -> Outdated (Target version: "v1.0.5") +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "craftpresence" -> Outdated (Target version: "v2.5.4") +[12:47:47] [Render thread/INFO] [Every Compat/]: Registered 42 compat blocks making up 0.31% of total blocks registered +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Found status: OUTDATED Current: 5.1.4.312 Target: 5.1.4.346 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/pickupnotifier.json +[12:47:47] [Render thread/INFO] [Moonlight/]: Initialized color sets in 104ms +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: KubeJS TFC configuration: +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: Debug mode enabled: false +[12:47:47] [Render thread/INFO] [MEGA Cells/]: Initialised AE2WT integration. +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Found status: UP_TO_DATE Current: 8.0.0 Target: null +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Starting version check at https://update.maxhenkel.de/forge/corpse +[12:47:47] [Worker-ResourceReload-0/INFO] [FirstPersonModel/]: Loading FirstPerson Mod +[12:47:47] [Worker-ResourceReload-4/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 1/2 +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loading patreon trails data... +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loading patreon wing data... +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Client-side libs ready to use! +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loaded 45 patreon trails. +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loaded 21 patreon wings. +[12:47:47] [Worker-ResourceReload-7/INFO] [EMI/]: [EMI] Discovered Sodium +[12:47:47] [Worker-ResourceReload-14/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 1/2 +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_wut +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_restock +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:restock_amounts +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Found status: OUTDATED Current: 1.20.1-1.0.19 Target: 1.20.1-1.0.20 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Starting version check at https://raw.githubusercontent.com/hlysine/create_connected/main/update.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Found status: OUTDATED Current: 0.8.2-mc1.20.1 Target: 1.0.1-mc1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Starting version check at https://api.modrinth.com/updates/rubidium-extra/forge_updates.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Found status: AHEAD Current: 3.1.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/hangglider.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Found status: UP_TO_DATE Current: 8.0.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Starting version check at https://updates.blamejared.com/get?n=searchables&gv=1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Found status: BETA Current: 1.0.3 Target: 1.0.3 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Starting version check at https://api.modrinth.com/updates/cc-tweaked/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Found status: OUTDATED Current: 1.113.1 Target: 1.115.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Found status: AHEAD Current: 1.0.2 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Found status: AHEAD Current: 2.5.0 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Starting version check at https://api.modrinth.com/updates/radium/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Found status: OUTDATED Current: 0.12.3+git.50c5c33 Target: 0.12.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Starting version check at https://updates.blamejared.com/get?n=attributefix&gv=1.20.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Found status: BETA Current: 21.0.4 Target: 21.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Starting version check at https://updates.blamejared.com/get?n=clumps&gv=1.20.1 +[12:47:49] [Worker-ResourceReload-4/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking patreon: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Found status: BETA Current: 12.0.0.4 Target: 12.0.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [catalogue] Starting version check at https://mrcrayfish.com/modupdatejson?id=catalogue +[12:47:50] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzlesaccessapi.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Found status: BETA Current: 8.0.7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: tfcambiental:snowshoes#inventory +java.io.FileNotFoundException: tfcambiental:models/item/snowshoes.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:266) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119306_(ModelBakery.java:384) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:150) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: carpeted:block/label +java.io.FileNotFoundException: carpeted:models/block/label.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:262) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:159) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Found status: OUTDATED Current: 47.2.6 Target: 47.4.0 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Starting version check at https://raw.githubusercontent.com/MehVahdJukaar/Moonlight/multi-loader/forge/update.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Found status: BETA Current: 1.20-2.13.51 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Starting version check at https://raw.githubusercontent.com/Toma1O6/UpdateSchemas/master/configuration-forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Found status: OUTDATED Current: 2.2.0 Target: 2.2.1 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Starting version check at https://github.com/AbdElAziz333/SmoothBoot-Reloaded/raw/mc1.20.1/dev/updates.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Found status: UP_TO_DATE Current: 0.0.4 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Starting version check at https://raw.githubusercontent.com/VidTu/Ksyxis/main/updater_ksyxis_forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Found status: OUTDATED Current: 1.3.2 Target: 1.3.3 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Starting version check at https://api.modrinth.com/updates/flywheel/forge_updates.json +[12:47:50] [Worker-ResourceReload-4/ERROR] [xa.ma.WorldMap/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Found status: BETA Current: 0.6.10-7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Starting version check at https://raw.githubusercontent.com/DmitryLovin/pluginUpdate/master/invupdate.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Found status: UP_TO_DATE Current: 3.4.26 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzleslib.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Found status: OUTDATED Current: 8.1.23 Target: 8.1.32 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Starting version check at https://api.modrinth.com/updates/betterf3/forge_updates.json +[12:47:50] [Render thread/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 2/2 +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Found status: UP_TO_DATE Current: 7.0.2 Target: null +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Starting version check at https://api.modrinth.com/updates/packet-fixer/forge_updates.json +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Found status: OUTDATED Current: 1.4.2 Target: 2.0.0 +[12:47:51] [Render thread/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: World Map found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Iris found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 2/2 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: New world map region cache hash code: -815523079 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: map_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Xaero's minimap found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Iris found! +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model firmaciv:firmaciv_compass#inventory: + minecraft:textures/atlas/blocks.png:minecraft:item/compass +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:tin_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model createaddition:small_light_connector#facing=west,mode=push,powered=true,rotation=x_clockwise_180,variant=default: + minecraft:textures/atlas/blocks.png:create:block/chute_block +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:copper_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +Reloading Dynamic Lights +[12:47:57] [Render thread/INFO] [co.jo.fl.ba.Backend/]: Loaded all shader sources. +Create Crafts & Additions Initialized! +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded values for 19 compatible attributes. +[12:47:57] [Worker-ResourceReload-2/ERROR] [AttributeFix/]: Attribute ID 'minecolonies:mc_mob_damage' does not belong to a known attribute. This entry will be ignored. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded 20 values from config. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Saving config file. 20 entries. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Applying changes for 20 attributes. +[12:47:57] [Worker-ResourceReload-11/INFO] [de.me.as.AstikorCarts/]: Automatic pull animal configuration: +pull_animals = [ + "minecraft:camel", + "minecraft:donkey", + "minecraft:horse", + "minecraft:mule", + "minecraft:skeleton_horse", + "minecraft:zombie_horse", + "minecraft:player", + "tfc:donkey", + "tfc:mule", + "tfc:horse" + ] +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at io.github.mortuusars.exposure.integration.jade.ExposureJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at de.maxhenkel.corpse.integration.waila.PluginCorpse +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at xfacthd.framedblocks.common.compat.jade.FramedJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.general.GeneralPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.create.CreatePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at me.pandamods.fallingtrees.compat.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.gregtechceu.gtceu.integration.jade.GTJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at net.dries007.tfc.compat.jade.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.vanilla.VanillaPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.universal.UniversalPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.core.CorePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at appeng.integration.modules.jade.JadeModule +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.glodblock.github.extendedae.xmod.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at cy.jdkdigital.treetap.compat.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.eerussianguy.firmalife.compat.tooltip.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.ljuangbminecraft.tfcchannelcasting.compat.JadeIntegration +[12:47:59] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'create_connected' took 1.342 s to run a deferred task. +[12:47:59] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:59] [Render thread/INFO] [mojang/Library]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: Sound engine started +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: [FANCYMENU] Reloading AudioResourceHandler after Minecraft SoundEngine reload.. +[12:47:59] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 4096x2048x4 minecraft:textures/atlas/blocks.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x512x4 minecraft:textures/atlas/signs.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x1024x4 minecraft:textures/atlas/chest.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh particle. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_translucent. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_armor_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull_z_offset. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_smooth_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_decal. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_no_outline. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named IViewRotMat in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named FogShape in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light0_Direction in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light1_Direction in the specified shader program. +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x0 minecraft:textures/atlas/particles.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:48:00] [Render thread/INFO] [xa.ma.WorldMap/]: Successfully reloaded the world map shaders! +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Loading exposure filters: +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_blue_pane, exposure:shaders/light_blue_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:orange_pane, exposure:shaders/orange_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:red_pane, exposure:shaders/red_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:purple_pane, exposure:shaders/purple_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:blue_pane, exposure:shaders/blue_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_gray_pane, exposure:shaders/light_gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:magenta_pane, exposure:shaders/magenta_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:gray_pane, exposure:shaders/gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:lime_pane, exposure:shaders/lime_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:green_pane, exposure:shaders/green_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:pink_pane, exposure:shaders/pink_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:yellow_pane, exposure:shaders/yellow_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:white_pane, exposure:shaders/white_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:brown_pane, exposure:shaders/brown_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:glass_pane, exposure:shaders/crisp.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:interplanar_projector, exposure:shaders/invert.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:black_pane, exposure:shaders/black_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:cyan_pane, exposure:shaders/cyan_tint.json] added. +[12:48:00] [Render thread/INFO] [patchouli/]: BookContentResourceListenerLoader preloaded 1073 jsons +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x128x0 computercraft:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x0 polylib:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 jei:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 moonlight:textures/atlas/map_markers.png-atlas +[12:48:00] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Successfully reloaded the minimap shaders! +[12:48:00] [Render thread/INFO] [Shimmer/]: buildIn shimmer configuration is enabled, this can be disabled by config file +[12:48:00] [Render thread/INFO] [Shimmer/]: mod jar and resource pack discovery: file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.ResourceHandlers/]: [FANCYMENU] Reloading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.pr.ResourcePreLoader/]: [FANCYMENU] Pre-loading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Updating animation sizes.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: FINISHED +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: title_screen +[12:48:00] [Render thread/INFO] [Oculus/]: Creating pipeline for dimension NamespacedId{namespace='minecraft', name='overworld'} +[12:48:01] [Render thread/INFO] [ambientsounds/]: Loaded AmbientEngine 'basic' v3.1.0. 11 dimension(s), 11 features, 11 blockgroups, 2 sound collections, 37 regions, 58 sounds, 11 sound categories, 5 solids and 2 biome types +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: PlayerAnimator not found! +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Vanilla Hands items: [] +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Auto Disable items: [camera] +[12:48:02] [Render thread/WARN] [ModernFix/]: Game took 40.304 seconds to start diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log new file mode 100644 index 000000000..51e5ec546 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log @@ -0,0 +1,2854 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG + + , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Outdated (Target version: "v2.5.4")]]> + +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. + + + + + Outdated (Target version: "v1.0.5")]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (ModelBakery.java:150) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + (ModelBakery.java:159) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Reloading Dynamic Lights + + + +Create Crafts & Additions Initialized! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt new file mode 100644 index 000000000..1b3002117 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt @@ -0,0 +1,945 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt new file mode 100644 index 000000000..941e5e3fe --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt @@ -0,0 +1,869 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO diff --git a/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt new file mode 100644 index 000000000..02734e56f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt @@ -0,0 +1,25 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.text.log b/tests/testdata/TestLogs/vanilla-1.21.5.text.log new file mode 100644 index 000000000..e78702858 --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.text.log @@ -0,0 +1,25 @@ +[12:50:56] [Datafixer Bootstrap/INFO]: 263 Datafixer optimizations took 908 milliseconds +[12:50:58] [Render thread/INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD] +[12:50:58] [Render thread/INFO]: Setting user: Ryexandrite +[12:50:58] [Render thread/INFO]: Backend library: LWJGL version 3.3.3+5 +[12:50:58] [Render thread/INFO]: Using optional rendering extensions: GL_KHR_debug, GL_ARB_vertex_attrib_binding, GL_ARB_direct_state_access +[12:50:58] [Render thread/INFO]: Reloading ResourceManager: vanilla +[12:50:59] [Worker-Main-6/INFO]: Found unifont_all_no_pua-16.0.01.hex, loading +[12:50:59] [Worker-Main-7/INFO]: Found unifont_jp_patch-16.0.01.hex, loading +[12:50:59] [Render thread/WARN]: minecraft:pipeline/entity_translucent_emissive shader program does not use sampler Sampler2 defined in the pipeline. This might be a bug. +[12:50:59] [Render thread/INFO]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:50:59] [Render thread/INFO]: Sound engine started +[12:50:59] [Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas +[12:50:59] [Render thread/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:50:59] [Render thread/INFO]: Created: 64x64x0 minecraft:textures/atlas/map_decorations.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/particles.png-atlas +[12:51:00] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:51:00] [Render thread/INFO]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:51:00] [Render thread/INFO]: Created: 1024x512x0 minecraft:textures/atlas/gui.png-atlas diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.xml.log b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log new file mode 100644 index 000000000..24bfe0b7f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +