diff --git a/.clang-format b/.clang-format index a0b93351f..005a1006b 100644 --- a/.clang-format +++ b/.clang-format @@ -16,3 +16,4 @@ BraceWrapping: BreakBeforeBraces: Custom BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false +QualifierAlignment: Left diff --git a/.clang-tidy b/.clang-tidy index ef5166da4..c5eb09533 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,23 +1,32 @@ -Checks: - - modernize-use-using - - readability-avoid-const-params-in-decls - - misc-unused-parameters, - - readability-identifier-naming +FormatStyle: file -# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. +Checks: + "bugprone-*,clang-analyzer-*,cppcoreguidelines-*,hicpp-*,misc-*,modernize-*,performance-*,portability-*,readability-*, + -*-magic-numbers, + -*-non-private-member-variables-in-classes, + -*-special-member-functions, + -bugprone-easily-swappable-parameters, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-type-static-cast-downcast, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -portability-avoid-pragma-once, + -readability-avoid-unconditional-preprocessor-if, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -readability-redundant-access-specifiers" 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 + misc-include-cleaner.MissingIncludes: false + readability-identifier-naming.DefaultCase: "camelBack" + readability-identifier-naming.NamespaceCase: "CamelCase" + readability-identifier-naming.ClassCase: "CamelCase" + readability-identifier-naming.ClassConstantCase: "CamelCase" + readability-identifier-naming.EnumCase: "CamelCase" + readability-identifier-naming.EnumConstantCase: "CamelCase" + readability-identifier-naming.MacroDefinitionCase: "UPPER_CASE" + readability-identifier-naming.ClassMemberPrefix: "m_" + readability-identifier-naming.StaticConstantPrefix: "s_" + readability-identifier-naming.StaticVariablePrefix: "s_" + readability-identifier-naming.GlobalConstantPrefix: "g_" + readability-implicit-bool-conversion.AllowPointerConditions: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ea1fbfdd9..3574bf20d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: File a bug report -labels: [bug] +labels: ["bug: unconfirmed", "status: needs triage"] body: - type: markdown attributes: @@ -23,14 +23,14 @@ body: - macOS - Linux - Other -- type: textarea +- type: input attributes: label: Version of Prism Launcher description: The version of Prism Launcher used in the bug report. placeholder: Prism Launcher 5.0 validations: required: true -- type: textarea +- type: input attributes: label: Version of Qt description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index fa7cdbe61..5e6d68e65 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -1,7 +1,7 @@ # Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst name: Request for Comment (RFC) description: Propose a larger change and start a discussion. -labels: [rfc] +labels: ["type: enhancement", "status: needs discussion", "status: needs triage"] body: - type: markdown attributes: @@ -44,8 +44,8 @@ body: attributes: label: Unresolved Questions description: | - Are there any portions of your proposal which need to be discussed with the community before the RFC can proceed? - Be careful here -- an RFC with a lot of remaining questions is likely to be stalled. + Are there any portions of your proposal which need to be discussed with the community before the RFC can proceed? + Be careful here -- an RFC with a lot of remaining questions is likely to be stalled. If your RFC is mostly unresolved questions and not too much substance, it may not be ready. placeholder: Do a lot of users care about the cat? validations: diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml index ddee86b65..18a202ae1 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -1,6 +1,6 @@ name: Suggestion description: Make a suggestion -labels: [enhancement] +labels: ["type: enhancement", "status: needs triage"] body: - type: markdown attributes: diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index e87d79665..2ce6ca955 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -27,6 +27,18 @@ runs: using: composite steps: + - name: Cleanup Qt installation on Linux + shell: bash + run: | + rm -rf "$QT_PLUGIN_PATH"/printsupport + rm -rf "$QT_PLUGIN_PATH"/sqldrivers + rm -rf "$QT_PLUGIN_PATH"/help + rm -rf "$QT_PLUGIN_PATH"/designer + rm -rf "$QT_PLUGIN_PATH"/qmltooling + rm -rf "$QT_PLUGIN_PATH"/qmlls + rm -rf "$QT_PLUGIN_PATH"/qmllint + rm -rf "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so + - name: Setup build variables shell: bash run: | @@ -52,7 +64,7 @@ runs: - name: Package AppImage shell: bash env: - VERSION: ${{ inputs.version }} + VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.version }} BUILD_DIR: build INSTALL_APPIMAGE_DIR: install-appdir @@ -63,7 +75,7 @@ runs: if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then echo "$GPG_PRIVATE_KEY" > privkey.asc gpg --import privkey.asc - gpg --export --armor 9C7A2C9B62603299 > pubkey.asc + gpg --export --armor ${{ inputs.gpg-private-key-id }} > pubkey.asc else echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY fi @@ -78,16 +90,26 @@ runs: # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + + #disable OpenGL and Vulkan launcher features until https://github.com/VHSgunzo/sharun/issues/35 + echo "PRISMLAUNCHER_DISABLE_GLVULKAN=1" >> "$INSTALL_APPIMAGE_DIR"/.env + #makes the launcher use portals for file picking + echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" >> "$INSTALL_APPIMAGE_DIR"/.env ln -s org.prismlauncher.PrismLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml ln -s share/applications/org.prismlauncher.PrismLauncher.desktop "$INSTALL_APPIMAGE_DIR" ln -s share/icons/hicolor/256x256/apps/org.prismlauncher.PrismLauncher.png "$INSTALL_APPIMAGE_DIR" mv "$INSTALL_APPIMAGE_DIR"/{sharun,AppRun} ls -la "$INSTALL_APPIMAGE_DIR" + if [[ "${{ github.ref_type }}" == "tag" ]]; then + APPIMAGE_DEST="PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage" + else + APPIMAGE_DEST="PrismLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" + fi + mkappimage \ --updateinformation "gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" \ "$INSTALL_APPIMAGE_DIR" \ - "PrismLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" + "$APPIMAGE_DEST" - name: Package portable tarball shell: bash @@ -108,24 +130,24 @@ runs: # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! find "$INSTALL_PORTABLE_DIR" -name '*gamemode*' -exec rm {} + - 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 + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f -o -type l); 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@v6 + uses: actions/upload-artifact@v7 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@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage - path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage + path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage - name: Upload AppImage Zsync - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync - path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync + path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage.zsync diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml index 1693ca21b..1af01250f 100644 --- a/.github/actions/package/macos/action.yml +++ b/.github/actions/package/macos/action.yml @@ -96,16 +96,36 @@ runs: fi ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + - name: Create DMG + shell: bash + env: + INSTALL_DIR: install + run: | + cd ${{ env.INSTALL_DIR }} + + mkdir -p src + cp -R "Prism Launcher.app" src/ + + ln -s /Applications src/ + + hdiutil create \ + -volname "Prism Launcher ${{ inputs.version }}" \ + -srcfolder src \ + -ov -format ULMO \ + "../PrismLauncher.dmg" + - 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) + signature_zip=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature_dmg=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.dmg -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\` + - :memo: Sparkle Signature (ed25519): \`$signature_zip\` (ZIP) + - :memo: Sparkle Signature (ed25519): \`$signature_dmg\` (DMG) EOF else cat >> $GITHUB_STEP_SUMMARY << EOF @@ -115,7 +135,13 @@ runs: fi - name: Upload binary tarball - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher.zip + + - name: Upload disk image + uses: actions/upload-artifact@v7 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}.dmg + path: PrismLauncher.dmg diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml index 49dfbd545..532f3db44 100644 --- a/.github/actions/package/windows/action.yml +++ b/.github/actions/package/windows/action.yml @@ -54,32 +54,33 @@ runs: 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: Emit warning for unsigned builds - if: ${{ github.ref_name != 'develop' || inputs.azure-client-id == '' }} + if: ${{ env.CI_HAS_ACCESS_TO_AZURE == '' || inputs.azure-client-id == '' }} shell: pwsh run: | ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - name: Login to Azure - if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }} - uses: azure/login@v2 + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} + uses: azure/login@v3 with: client-id: ${{ inputs.azure-client-id }} tenant-id: ${{ inputs.azure-tenant-id }} subscription-id: ${{ inputs.azure-subscription-id }} - name: Sign executables - if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }} - uses: azure/trusted-signing-action@v0 + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} + uses: azure/artifact-signing-action@v2 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher certificate-profile-name: PrismLauncher - - files: | - ${{ github.workspace }}\install\prismlauncher.exe - ${{ github.workspace }}\install\prismlauncher_filelink.exe - ${{ github.workspace }}\install\prismlauncher_updater.exe - + files-folder: ${{ github.workspace }}\install\ + files-folder-filter: dll,exe + files-folder-recurse: true + files-folder-depth: 2 + # recommended in https://github.com/Azure/artifact-signing-action#timestamping-1 + timestamp-rfc3161: 'http://timestamp.acs.microsoft.com' + timestamp-digest: 'SHA256' # TODO(@getchoo): Is this all really needed??? # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md exclude-environment-credential: true @@ -141,7 +142,7 @@ runs: - name: Sign installer if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} - uses: azure/trusted-signing-action@v0 + uses: azure/artifact-signing-action@v2 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher @@ -150,6 +151,9 @@ runs: files: | ${{ github.workspace }}\PrismLauncher-Setup.exe + # recommended in https://github.com/Azure/artifact-signing-action#timestamping-1 + timestamp-rfc3161: 'http://timestamp.acs.microsoft.com' + timestamp-digest: 'SHA256' # TODO(@getchoo): Is this all really needed??? # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md exclude-environment-credential: true @@ -164,19 +168,19 @@ runs: exclude-interactive-browser-credential: true - name: Upload binary zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: install/** - name: Upload portable zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: install-portable/** - name: Upload installer - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 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 index a8ecba583..b73c7509a 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -55,7 +55,7 @@ runs: # 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.20 + uses: hendrikmuhs/ccache-action@v1.2.23 with: variant: sccache create-symlink: ${{ runner.os != 'Windows' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 753ea19fe..fa5af702b 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -1,4 +1,5 @@ name: Setup Linux dependencies +description: Install and setup dependencies for building Prism Launcher runs: using: composite diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml index 7de08e261..a90544be0 100644 --- a/.github/actions/setup-dependencies/macos/action.yml +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -44,4 +44,4 @@ runs: - name: Setup vcpkg environment shell: bash run: | - echo "CMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> "$GITHUB_ENV" + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index b250b0088..24ad51d8f 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -1,4 +1,5 @@ name: Setup Windows Dependencies +description: Install and setup dependencies for building Prism Launcher inputs: build-type: @@ -57,7 +58,7 @@ runs: if: ${{ inputs.msystem == '' }} shell: bash run: | - echo "CMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> "$GITHUB_ENV" + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" - name: Setup MSYS2 (MinGW) if: ${{ inputs.msystem != '' }} @@ -90,7 +91,7 @@ runs: - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - uses: actions/cache@v5.0.1 + uses: actions/cache@v5.0.5 with: path: '${{ github.workspace }}\.ccache' key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 899f654fe..9cde3307d 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -8,8 +8,7 @@ on: # the GitHub repository. This means that it should not evaluate user input in a # way that allows code injection. -permissions: - contents: read +permissions: {} jobs: backport: @@ -19,13 +18,13 @@ jobs: actions: write # for korthout/backport-action to create PR with workflow changes name: Backport Pull Request if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v4.0.0 + uses: korthout/backport-action@v4.5 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 index ecbaf755d..001080154 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -14,15 +14,17 @@ on: required: true type: number +permissions: {} + jobs: blocked_status: name: Check Blocked Status - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Generate token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} @@ -76,11 +78,11 @@ jobs: run: | prs=$( jq -c ' - .prBody as $body + .prBody as $body | ( - $body | - reduce ( - . | scan("blocked (?:by|on):? #([0-9]+)") + $body | + reduce ( + . | scan("[Bb]locked (?:[Bb]y|[Oo]n):? #([0-9]+)") | map({ "type": "Blocked on", "number": ( . | tonumber ) @@ -88,17 +90,17 @@ jobs: ) as $i ([]; . + [$i[]]) ) as $bprs | ( - $body | - reduce ( - . | scan("stacked on:? #([0-9]+)") + $body | + reduce ( + . | scan("[Ss]tacked [Oo]n:? #([0-9]+)") | map({ "type": "Stacked on", "number": ( . | tonumber ) }) ) as $i ([]; . + [$i[]]) ) as $sprs - | ($bprs + $sprs) as $prs - | { + | ($bprs + $sprs) as $prs + | { "blocking": $prs, "numBlocking": ( $prs | length), } @@ -126,7 +128,7 @@ jobs: "number": .number, "merged": .merged, "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end), - "labels": (reduce .labels[].name as $l ([]; . + [$l])), + "labels": (reduce .labels[].name as $l ([]; . + [$l])), "basePrUrl": .html_url, "baseRepoName": .head.repo.name, "baseRepoOwner": .head.repo.owner.login, @@ -143,19 +145,19 @@ jobs: echo "current_blocking=$(jq -c 'map( select( (.type == "Stacked on" and (.merged | not)) or - (.type == "Blocked on" and (.state == "Open")) + (.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) + if: "(fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'status: 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" + gh -R ${{ github.repository }} issue edit --add-label 'status: blocked' "$PR_NUMBER" - name: Remove 'blocked' Label if All Dependencies Are Merged id: unlabel_blocked @@ -164,7 +166,7 @@ jobs: env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | - gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER" + gh -R ${{ github.repository }} issue edit --remove-label 'status: blocked' "$PR_NUMBER" - name: Apply 'blocking' Label to Unmerged Dependencies id: label_blocking @@ -175,7 +177,7 @@ jobs: 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 + gh -R ${{ github.repository }} issue edit --add-label 'status: blocking' "$pr" || true done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") - name: Apply Blocking PR Status Check diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eaf9e49ef..0596906c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,57 +5,9 @@ concurrency: cancel-in-progress: true on: - push: - branches: - - "develop" - - "release-*" - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/workflows/build.yml" - - ".github/actions/package/**" - - ".github/actions/setup-dependencies/**" + merge_group: + types: [checks_requested] pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/workflows/build.yml" - - ".github/actions/package/**" - - ".github/actions/setup-dependencies/**" workflow_call: inputs: build-type: @@ -72,6 +24,8 @@ on: type: string default: Debug +permissions: {} + jobs: build: name: Build (${{ matrix.artifact-name }}) @@ -79,6 +33,7 @@ jobs: environment: ${{ inputs.environment || '' }} permissions: + contents: read # Required for Azure Trusted Signing id-token: write # Required for vcpkg binary cache @@ -91,12 +46,12 @@ jobs: - os: ubuntu-24.04 artifact-name: Linux cmake-preset: linux - qt-version: 6.10.1 + qt-version: 6.10.2 - os: ubuntu-24.04-arm artifact-name: Linux-aarch64 cmake-preset: linux - qt-version: 6.10.1 + qt-version: 6.10.2 - os: windows-2022 artifact-name: Windows-MinGW-w64 @@ -115,13 +70,13 @@ jobs: cmake-preset: windows_msvc # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! vcvars-arch: amd64 - qt-version: 6.10.1 + qt-version: 6.10.2 - os: windows-11-arm artifact-name: Windows-MSVC-arm64 cmake-preset: windows_msvc vcvars-arch: arm64 - qt-version: 6.10.1 + qt-version: 6.10.2 - os: macos-26 artifact-name: macOS @@ -220,6 +175,8 @@ jobs: - name: Package (Windows) if: ${{ runner.os == 'Windows' }} uses: ./.github/actions/package/windows + env: + CI_HAS_ACCESS_TO_AZURE: ${{ vars.CI_HAS_ACCESS_TO_AZURE || '' }} with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 000000000..d72994c50 --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,48 @@ +name: Clang-Tidy Code Scanning + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + merge_group: + types: [checks_requested] + pull_request: + +permissions: {} + +jobs: + clang-tidy: + name: Run Clang-Tidy + + runs-on: ubuntu-latest + + permissions: + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Required for diffing later on + submodules: "true" + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Run source generators + # TODO(@getchoo): Figure out how to make this work with PCH + run: | + nix develop --command bash -c ' + cmake -B build -D Launcher_USE_PCH=OFF && cmake --build build --target autogen autorcc + ' + + # TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed + - name: Run clang-tidy-diff + env: + BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + run: | + nix develop --command bash -c ' + clang-tidy -verify-config && git diff -U0 --no-color "$BASE_REF" | clang-tidy-diff.py -p1 -quiet -only-check-in-db + ' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8a5fa26fb..f9705bf53 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,63 +5,21 @@ concurrency: cancel-in-progress: true on: - push: - branches: - - "develop" - - "release-*" - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/codeql/**" - - ".github/workflows/codeql.yml" - - ".github/actions/setup-dependencies/**" + merge_group: + types: [checks_requested] pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/codeql/**" - - ".github/workflows/codeql.yml" - - ".github/actions/setup-dependencies/**" workflow_dispatch: +permissions: {} + jobs: CodeQL: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -79,12 +37,16 @@ jobs: uses: ./.github/actions/setup-dependencies with: build-type: Debug - qt-version: 6.10.1 + qt-version: 6.4.3 - name: Configure and Build run: | - cmake --preset linux + cmake --preset linux -DLauncher_USE_PCH=OFF cmake --build --preset linux --config Debug + - name: Run tests + run: | + ctest --preset linux --build-config Debug --extra-verbose --output-on-failure + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..7af2c1ccb --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,174 @@ +name: Development Container + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + merge_group: + types: [checks_requested] + pull_request: + workflow_dispatch: + +permissions: {} + +env: + REGISTRY: ghcr.io + +jobs: + build: + name: Build (${{ matrix.arch }}) + + permissions: + contents: read + packages: write + + outputs: + image-name: ${{ steps.image-name.outputs.image-name }} + + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + os: ubuntu-24.04-arm + - arch: amd64 + os: ubuntu-24.04 + + runs-on: ${{ matrix.os }} + + steps: + - name: Set image name + id: image-name + run: | + echo "image-name=${REGISTRY}/${GITHUB_REPOSITORY_OWNER,,}/devcontainer" >> "$GITHUB_OUTPUT" + + - name: Install Podman + uses: redhat-actions/podman-install@main + # TODO(@getchoo): Always use this when the action properly supports ARM + if: ${{ runner.arch == 'X64' || runner.arch == 'X86' }} + with: + github-token: ${{ github.token }} + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Determine metadata for image + id: image-metadata + uses: docker/metadata-action@v6 + with: + images: | + ${{ steps.image-name.outputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=latest,enable=${{ github.event.merge_group.base_ref == 'refs/heads/develop' }} + + type=sha + type=sha,format=long + type=ref,event=branch + type=ref,event=tag + + - name: Build image + id: build-image + uses: redhat-actions/buildah-build@v2 + with: + containerfiles: | + ./Containerfile + tags: ${{ steps.image-metadata.outputs.tags }} + labels: ${{ steps.image-metadata.outputs.labels }} + + - name: Push image + id: push-image + if: ${{ github.event_name != 'pull_request' }} + uses: redhat-actions/push-to-registry@v2 + with: + tags: ${{ steps.build-image.outputs.tags }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + tls-verify: true + + - name: Export image digest + if: ${{ github.event_name != 'pull_request' }} + env: + DIGEST: ${{ steps.push-image.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP"/digests + touch "$RUNNER_TEMP"/digests/"${DIGEST#sha256:}" + + - name: Upload digest artifact + if: ${{ github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v7 + with: + name: digests-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + manifest: + name: Create manifest + + needs: [ build ] + if: ${{ github.event_name != 'pull_request' }} + + permissions: + contents: read + packages: write + + runs-on: ubuntu-24.04 + + steps: + - name: Download digests + uses: actions/download-artifact@v8 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Install Podman + # TODO(@getchoo): Always use this when the action properly supports ARM + if: ${{ runner.arch == 'X64' || runner.arch == 'X86' }} + uses: redhat-actions/podman-install@main + with: + github-token: ${{ github.token }} + + - name: Login to registry + uses: redhat-actions/podman-login@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Determine metadata for manifest + id: manifest-metadata + uses: docker/metadata-action@v6 + with: + images: | + ${{ needs.build.outputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=latest,enable=${{ github.event.merge_group.base_ref == 'refs/heads/develop' }} + + type=sha + type=sha,format=long + type=ref,event=branch + type=ref,event=tag + + - name: Create manifest list + working-directory: ${{ runner.temp }}/digests + env: + IMAGE_NAME: ${{ needs.build.outputs.image-name }} + run: | + while read -r tag; do + podman manifest create "$tag" \ + $(printf "$IMAGE_NAME@sha256:%s " *) + done <<< "$DOCKER_METADATA_OUTPUT_TAGS" + + - name: Push manifest + uses: redhat-actions/push-to-registry@v2 + with: + tags: ${{ steps.manifest-metadata.outputs.tags }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + tls-verify: true diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml deleted file mode 100644 index e3fbb2c32..000000000 --- a/.github/workflows/flatpak.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Flatpak - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - push: - branches: - - "develop" - - "release-*" - # We don't do anything with these artifacts on releases. They go to Flathub - tags-ignore: - - "*" - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # 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" - - "**.java" - - "**.ui" - - # 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 - - runs-on: ${{ matrix.os }} - - container: - image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.10 - options: --privileged - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - 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 index d37c33761..3542a470e 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -11,19 +11,21 @@ on: required: true type: number +permissions: {} + jobs: update-blocked-status: name: Update Blocked Status - runs-on: ubuntu-latest + runs-on: ubuntu-slim # 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') }} + if: "${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'status: blocking') }}" steps: - name: Generate token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} @@ -35,10 +37,10 @@ jobs: PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} run: | blocked_prs=$( - gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ + gh -R ${{ github.repository }} pr list --label 'status: blocked' --json 'number,body' \ | jq -c --argjson pr "$PR_NUMBER" ' reduce ( .[] | select( - .body | + .body | scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | map(tonumber) | any(.[]; . == $pr) @@ -47,7 +49,7 @@ jobs: ) { echo "deps=$blocked_prs" - echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" + echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" } >> "$GITHUB_OUTPUT" - name: Trigger Blocked PR Workflows for Dependants diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 2035668f4..58f4d263a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -17,6 +17,7 @@ on: - "**.h" - "**.java" - "**.ui" + - "**.md" # Build files - "**.nix" @@ -33,7 +34,6 @@ on: # Files - "CMakeLists.txt" - - "COPYING.md" # Workflows - ".github/workflows/nix.yml" @@ -44,6 +44,7 @@ on: - "**.h" - "**.java" - "**.ui" + - "**.md" # Build files - "**.nix" @@ -60,14 +61,12 @@ on: # Files - "CMakeLists.txt" - - "COPYING.md" # Workflows - ".github/workflows/nix.yml" workflow_dispatch: -permissions: - contents: read +permissions: {} env: DEBUG: ${{ github.ref_type != 'tag' }} @@ -76,6 +75,9 @@ jobs: build: name: Build (${{ matrix.system }}) + permissions: + contents: read + strategy: fail-fast: false matrix: @@ -86,7 +88,7 @@ jobs: - os: ubuntu-22.04-arm system: aarch64-linux - - os: macos-14 + - os: macos-26 system: aarch64-darwin runs-on: ${{ matrix.os }} @@ -101,7 +103,7 @@ jobs: # For PRs - name: Setup Nix Magic Cache if: ${{ github.event_name == 'pull_request' }} - uses: DeterminateSystems/magic-nix-cache-action@v13 + uses: DeterminateSystems/magic-nix-cache-action@v14 with: diagnostic-endpoint: "" use-flakehub: false @@ -109,7 +111,7 @@ jobs: # For in-tree builds - name: Setup Cachix if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - uses: cachix/cachix-action@v16 + uses: cachix/cachix-action@v17 with: name: prismlauncher authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a7da812e..1bb1c5b50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,14 +4,16 @@ on: release: types: [ released ] -permissions: - contents: read +permissions: {} jobs: winget: name: Winget - runs-on: windows-latest + permissions: + contents: read + + runs-on: ubuntu-slim steps: - name: Publish on Winget diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd22fe05c..e332488c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,18 @@ on: tags: - "*" +permissions: {} + jobs: build_release: name: Build Release uses: ./.github/workflows/build.yml + permissions: + contents: read + # Required for Azure Trusted Signing + id-token: write + # Required for vcpkg binary cache + packages: write with: build-type: Release environment: Release @@ -16,7 +24,9 @@ jobs: create_release: needs: build_release - runs-on: ubuntu-latest + permissions: + contents: write + runs-on: ubuntu-slim outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -26,7 +36,7 @@ jobs: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") @@ -41,6 +51,7 @@ jobs: mv PrismLauncher-*.AppImage/PrismLauncher-*-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-aarch64.AppImage.zsync PrismLauncher-Linux-aarch64.AppImage.zsync mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip + mv PrismLauncher-macOS*/PrismLauncher.dmg PrismLauncher-macOS-${{ env.VERSION }}.dmg tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -83,7 +94,7 @@ jobs: - name: Create release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} @@ -111,4 +122,5 @@ jobs: PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-macOS-${{ env.VERSION }}.zip + PrismLauncher-macOS-${{ env.VERSION }}.dmg PrismLauncher-${{ env.VERSION }}.tar.gz diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index b8f8137d7..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,29 +0,0 @@ -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@v10 - 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/update-flake.yml b/.github/workflows/update-flake.yml index 7f8ca2a0d..fa3e3b4d3 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -6,25 +6,30 @@ on: - cron: "0 0 * * 0" workflow_dispatch: -permissions: - contents: write - pull-requests: write +permissions: {} jobs: update-flake: if: github.repository == 'PrismLauncher/PrismLauncher' - runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31 + - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31 - uses: DeterminateSystems/update-flake-lock@v28 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" pr-labels: | - Linux - packaging - simple change + platform: Linux + area: packaging + complexity: low + priority: low + type: robot changelog:omit diff --git a/.gitmodules b/.gitmodules index f2d71ef29..42c566fa8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git -[submodule "flatpak/shared-modules"] - path = flatpak/shared-modules - url = https://github.com/flathub/shared-modules.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8816be82e..12afdefcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22) # minimum version required by Qt +cmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` project(Launcher LANGUAGES C CXX) if(APPLE) @@ -13,6 +13,10 @@ endif() ##################################### Set CMake options ##################################### set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOGEN_ORIGIN_DEPENDS OFF) +set(CMAKE_GLOBAL_AUTOGEN_TARGET ON) +set(CMAKE_GLOBAL_AUTORCC_TARGET ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") @@ -30,79 +34,82 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -if(MSVC) - # /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag - # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 - # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs - set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}") - # /EHs Enables stack unwind semantics for standard C++ exceptions to ensure stackframes are unwound - # and object deconstructors are called when an exception is caught. - # without it memory leaks and a warning is printed - # /EHc tells the compiler to assume that functions declared as extern "C" never throw a C++ exception - # This appears to not always be a defualt compiler option in CMAKE - set(CMAKE_CXX_FLAGS "/EHsc ${CMAKE_CXX_FLAGS}") +add_compile_definitions($<$>:QT_NO_DEBUG>) +add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x060400) +add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) - # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs - # This implicitly selects an entrypoint specific to the subsystem selected - # qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs - # Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM - # This allows tests to still use have console without using seperate linker flags - # /LTCG allows for linking wholy optimizated programs - # /MANIFEST:NO disables generating a manifest file, we instead provide our own - # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB - set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") +if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + add_compile_options( + # /GS Adds buffer security checks, default on but included anyway to mirror gcc's fstack-protector flag + "$<$:/GS>" + + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard + "$<$,$>:/Gw;/Gy;/guard:cf>" + ) + + add_link_options( + # /LTCG allows for linking wholy optimizated programs + # /MANIFEST:NO disables generating a manifest file, we instead provide our own + # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB + "$<$:/LTCG;/MANIFEST:NO;/STACK:8388608>" + ) # /GL enables whole program optimizations - # /Gw helps reduce binary size - # /Gy allows the compiler to package individual functions - # /guard:cf enables control flow guard - foreach(lang C CXX) - set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf") - endforeach() + # NOTE: With Clang, this is implemented as regular LTO and only used during linking + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options("$<$,$>:/GL>") + endif() # See https://github.com/ccache/ccache/issues/1040 - # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT - # See https://cmake.org/cmake/help/v3.25/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html - foreach(config DEBUG RELWITHDEBINFO) - foreach(lang C CXX) - set(flags_var "CMAKE_${lang}_FLAGS_${config}") - string(REGEX REPLACE "/Z[Ii]" "/Z7" ${flags_var} "${${flags_var}}") - endforeach() - endforeach() + # TODO(@getchoo): Is sccache affected by this? Would be nice to use `ProgramDatabase`.... + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") endif() else() - set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") + add_compile_options("$<$:-fstack-protector-strong;--param=ssp-buffer-size=4>") + + # Avoid re-defining _FORTIFY_SOURCE, as it can cause redefinition errors in setups that use it by default (i.e., package builds) + if(NOT (CMAKE_C_FLAGS MATCHES "-D_FORTIFY_SOURCE" OR CMAKE_CXX_FLAGS MATCHES "-D_FORTIFY_SOURCE")) + # NOTE: _FORTIFY_SOURCE requires optimizations in most newer versions of glibc + add_compile_options("$<$,$>:-D_FORTIFY_SOURCE=2>") + endif() # 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}") + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_link_options("$<$:-Wl,--stack,8388608>") # -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() + add_compile_options("$<$,$>:-ffunction-sections;-fdata-sections;-mguard=cf>") endif() endif() -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") - -# 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 MATCHES "^Ninja") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) endif() +option(USE_CLANG_TIDY "Enable the use of clang-tidy during compilation" OFF) + +if(USE_CLANG_TIDY) + find_program(CLANG_TIDY clang-tidy OPTIONAL) + if(CLANG_TIDY) + message(STATUS "Using clang-tidy during compilation") + set(CLANG_TIDY_COMMAND "${CLANG_TIDY}" "--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy") + set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_COMMAND}) + else() + message(WARNING "Unable to find `clang-tidy`. Not using during compilation") + endif() +endif() + option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser @@ -139,8 +146,9 @@ if(ENABLE_LTO) if(ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE) if(CMAKE_BUILD_TYPE) - if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "IPO / LTO enabled") else() message(STATUS "Not enabling IPO / LTO on debug builds") @@ -169,12 +177,13 @@ endif() ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") -set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") +set(Launcher_WIKI_URL "https://prismlauncher.org/wiki/" CACHE STRING "URL that gets opened when the user clicks 'Launcher Help'") +set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") -set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") +set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 10) +set(Launcher_VERSION_MAJOR 12) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_PATCH 0) @@ -216,6 +225,8 @@ set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL # Builds set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") +option(Launcher_USE_PCH "Use precompiled headers where possible" ON) + # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) @@ -281,7 +292,7 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") # Find the required Qt parts if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) + find_package(Qt6 6.4 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) else() @@ -363,7 +374,7 @@ if(UNIX AND APPLE) # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") - set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.prismlauncher.${Launcher_Name}") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "${Launcher_AppID}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") @@ -408,7 +419,7 @@ if(UNIX AND APPLE) COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" --compile "${ASSETS_OUT_DIR}" --output-partial-info-plist /dev/null - --app-icon PrismLauncher + --app-icon ${Launcher_Name} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} @@ -425,7 +436,7 @@ if(UNIX AND APPLE) else() message(WARNING "actool not found. Cannot compile macOS app icons.\n" - "Install Xcode command line tools: 'xcode-select --install'") + "Install Xcode command line tools: 'xcode-select --install'") endif() @@ -443,7 +454,7 @@ elseif(UNIX) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png") - install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") @@ -481,7 +492,6 @@ option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. 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 diff --git a/CMakePresets.json b/CMakePresets.json index 613fada07..f8496acb6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,214 +1,222 @@ { - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "cmakeMinimumRequired": { - "major": 3, - "minor": 28 - }, - "configurePresets": [ - { - "name": "base", - "hidden": true, - "binaryDir": "build", - "installDir": "install", - "generator": "Ninja Multi-Config", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", - "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", - "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", - "ENABLE_LTO": "ON" - } - }, - { - "name": "linux", - "displayName": "Linux", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - } - }, - { - "name": "macos", - "displayName": "macOS", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - } - }, - { - "name": "macos_universal", - "displayName": "macOS (Universal Binary)", - "inherits": [ - "macos" - ], - "cacheVariables": { - "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", - "VCPKG_TARGET_TRIPLET": "universal-osx" - } - }, - { - "name": "windows_mingw", - "displayName": "Windows (MinGW)", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "windows_msvc", - "displayName": "Windows (MSVC)", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - } - ], - "buildPresets": [ - { - "name": "linux", - "displayName": "Linux", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - }, - "configurePreset": "linux" - }, - { - "name": "macos", - "displayName": "macOS", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - }, - "configurePreset": "macos" - }, - { - "name": "macos_universal", - "displayName": "macOS (Universal Binary)", - "inherits": [ - "macos" - ], - "configurePreset": "macos_universal" - }, - { - "name": "windows_mingw", - "displayName": "Windows (MinGW)", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "configurePreset": "windows_mingw" - }, - { - "name": "windows_msvc", - "displayName": "Windows (MSVC)", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "configurePreset": "windows_msvc" - } - ], - "testPresets": [ - { - "name": "base", - "hidden": true, - "output": { - "outputOnFailure": true - }, - "execution": { - "noTestsAction": "error" - }, - "filter": { - "exclude": { - "name": "^example64|example$" - } - } - }, - { - "name": "linux", - "displayName": "Linux", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - }, - "configurePreset": "linux" - }, - { - "name": "macos", - "displayName": "macOS", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - }, - "configurePreset": "macos" - }, - { - "name": "macos_universal", - "displayName": "macOS (Universal Binary)", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - }, - "configurePreset": "macos_universal" - }, - { - "name": "windows_mingw", - "displayName": "Windows (MinGW)", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "configurePreset": "windows_mingw" - }, - { - "name": "windows_msvc", - "displayName": "Windows (MSVC)", - "inherits": [ - "base" - ], - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "configurePreset": "windows_msvc" - } - ] + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build", + "installDir": "install", + "generator": "Ninja Multi-Config", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", + "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", + "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", + "ENABLE_LTO": "ON" + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "VCPKG_TARGET_TRIPLET": "universal-osx" + } + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ], + "buildPresets": [ + { + "name": "linux", + "displayName": "Linux", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "macos", + "displayName": "macOS", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "configurePreset": "macos_universal" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + } + ], + "testPresets": [ + { + "name": "base", + "hidden": true, + "output": { + "outputOnFailure": true, + "verbosity": "extra" + }, + "execution": { + "noTestsAction": "error" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos_universal" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + } + ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdc79faf2..f4b12d08b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,52 @@ # Contributions Guidelines +## Restrictions on Generative AI Usage (AI Policy) + +> [!NOTE] +> The following is adapted from [matplotlib's contributing guide](https://matplotlib.org/devdocs/devel/contribute.html#generative-ai) and the [Linux Kernel policy guide](https://www.kernel.org/doc./html/next/process/coding-assistants.html) + +We expect authentic engagement in our community. + +- Do not post output from Large Language Models or similar generative AI as comments on GitHub or our discord server, as such comments tend to be formulaic and low-quality content. +- If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach. + +Make sure you have added value based on your personal competency to your contributions. +Just taking some input, feeding it to an AI and posting the result is not of value to the project. +To preserve precious core developer capacity, we reserve the right to rigorously reject seemingly AI generated low-value contributions. + +### Signed-off-by and Developer Certificate of Origin + +AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer Certificate of Origin (DCO). The human submitter is responsible for: + +- Reviewing all AI-generated code +- Ensuring compliance with licensing requirements +- Adding their own Signed-off-by tag to certify the DCO +- Taking full responsibility for the contribution + +See [Signing your work](#signing-your-work) for more information. + +### Attribution + +When AI tools contribute to development, proper attribution helps track the evolving role of AI in the development process. Contributions should include an Assisted-by tag in the commit message with the following format: + +```text +Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2] +``` + +Where: + +- `AGENT_NAME` is the name of the AI tool or framework +- `MODEL_VERSION` is the specific model version used +- `[TOOL1] [TOOL2]` are optional specialized analysis tools used (e.g., coccinelle, sparse, smatch, clang-tidy) + +Basic development tools (git, gcc, make, editors) should not be listed. + +Example: + +```text +Assisted-by: Claude:claude-3-opus coccinelle sparse +``` + ## Code style All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! @@ -13,7 +60,8 @@ Please also follow the project's conventions for C++: - 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`. +- `const` global variables and macros should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`. +- enum constants should be formatted as `PascalCase`: `CamelusBactrianus` - Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`. - Avoid using `[[nodiscard]]` unless ignoring the return value is likely to cause a bug in cases such as: - A function allocates memory or another resource and the caller needs to clean it up. @@ -30,7 +78,7 @@ Here is what these conventions with the formatting configuration look like: constexpr double PI = 3.14159; -enum class PizzaToppings { HAM_AND_PINEAPPLE, OREO_AND_KETCHUP }; +enum class PizzaToppings { HamAndPineapple, OreoAndKetchup }; struct Person { QString name; diff --git a/COPYING.md b/COPYING.md index fb33844f7..52f29f2e6 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## Prism Launcher Prism Launcher - Minecraft Launcher - Copyright (C) 2022-2025 Prism Launcher Contributors + Copyright (C) 2022-2026 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 diff --git a/Containerfile b/Containerfile new file mode 100644 index 000000000..59595fe55 --- /dev/null +++ b/Containerfile @@ -0,0 +1,74 @@ +ARG DEBIAN_VERSION=stable-slim + +FROM docker.io/library/debian:${DEBIAN_VERSION} + +ARG QT_ABI=gcc_64 +ARG QT_ARCH= +ARG QT_HOST=linux +ARG QT_TARGET=desktop +ARG QT_VERSION=6.10.2 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get --assume-yes upgrade \ + && apt-get --assume-yes autopurge + +# Use Adoptium for Java 17 +RUN apt-get --assume-yes --no-install-recommends install \ + apt-transport-https ca-certificates curl gpg +RUN curl -L https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg +RUN echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list +RUN apt-get update + +# Install base dependencies +RUN apt-get --assume-yes --no-install-recommends install \ + # Compilers + clang lld llvm temurin-17-jdk \ + # Build system + cmake ninja-build extra-cmake-modules pkg-config \ + # Dependencies + cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev scdoc zlib1g-dev \ + # Tooling + clang-format clang-tidy git + +# Use LLD by default for faster linking +ENV CMAKE_LINKER_TYPE=lld + +# Prepare and install Qt +## Setup UTF-8 locale (required, apparently) +RUN apt-get --assume-yes --no-install-recommends install locales +RUN echo "C.UTF-8 UTF-8" > /etc/locale.gen +RUN locale-gen +ENV LC_ALL=C.UTF-8 + +## Some libraries are required for the official binaries +RUN apt-get --assume-yes --no-install-recommends install \ + libglib2.0-0t64 libxkbcommon0 python3-pip + +RUN pip3 install --break-system-packages aqtinstall +RUN aqt install-qt \ + ${QT_HOST} ${QT_TARGET} ${QT_VERSION} ${QT_ARCH} \ + --outputdir /opt/qt \ + --modules qtimageformats qtnetworkauth + +ENV PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/bin:$PATH +ENV QT_PLUGIN_PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/plugins/ + +## We don't use these. Nuke them +RUN rm -rf \ + "$QT_PLUGIN_PATH"/designer \ + "$QT_PLUGIN_PATH"/help \ + # "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so \ + "$QT_PLUGIN_PATH"/printsupport \ + "$QT_PLUGIN_PATH"/qmllint \ + "$QT_PLUGIN_PATH"/qmlls \ + "$QT_PLUGIN_PATH"/qmltooling \ + "$QT_PLUGIN_PATH"/sqldrivers + +# Setup workspace +RUN mkdir /work +WORKDIR /work + +ENTRYPOINT ["bash"] +CMD ["-i"] diff --git a/README.md b/README.md index 850370a29..ac6cfd96b 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,13 @@ Please understand that these builds are not intended for most users. There may b There are development builds available through: - [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contributors) -- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/build/develop) (this will always point only to the latest version of develop) +- [nightly.link](https://prismlauncher.org/nightly) (this will always point only to the latest version of develop) These have debug information in the binaries, so their file sizes are relatively larger. Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**. -For **Arch**, **Debian**, **Fedora**, **OpenSUSE (Tumbleweed)** and **Gentoo**, respectively, you can use these packages for the latest development versions: - -[![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-git) [![prismlauncher-git](https://img.shields.io/badge/aur-prismlauncher--qt5--git-1793D1?label=AUR&logo=archlinux&logoColor=white)](https://aur.archlinux.org/packages/prismlauncher-qt5-git) [![prismlauncher-git](https://img.shields.io/badge/mpr-prismlauncher--git-A80030?label=MPR&logo=debian&logoColor=white)](https://mpr.makedeb.org/packages/prismlauncher-git)
[![prismlauncher-nightly](https://img.shields.io/badge/copr-prismlauncher--nightly-51A2DA?label=COPR&logo=fedora&logoColor=white)](https://copr.fedorainfracloud.org/coprs/g3tchoo/prismlauncher/) [![prismlauncher-nightly](https://img.shields.io/badge/OBS-prismlauncher--nightly-3AB6A9?logo=opensuse&logoColor=white)](https://build.opensuse.org/project/show/home:getchoo) [![prismlauncher-9999](https://img.shields.io/badge/gentoo-prismlauncher--9999-4D4270?label=Gentoo&logo=gentoo&logoColor=white)](https://packages.gentoo.org/packages/games-action/prismlauncher) - -These packages are also available to all the distributions based on the ones mentioned above. +On Linux, we also offer our own [Flatpak nightly repository](https://github.com/PrismLauncher/flatpak). Most software centers are able to install it by opening [this link](https://flatpak.prismlauncher.org/prismlauncher-nightly.flatpakref). ## Community & Support @@ -61,12 +57,7 @@ The translation effort for Prism Launcher is hosted on [Weblate](https://hosted. ## Building -If you want to build Prism Launcher yourself, check the build instructions: - -- [Windows](https://prismlauncher.org/wiki/development/build-instructions/windows/) -- [Linux](https://prismlauncher.org/wiki/development/build-instructions/linux/) -- [MacOS](https://prismlauncher.org/wiki/development/build-instructions/macos/) -- [OpenBSD](https://prismlauncher.org/wiki/development/build-instructions/openbsd/) +If you want to build Prism Launcher yourself, check the [build instructions](https://prismlauncher.org/wiki/development/build-instructions). ## Sponsors & Partners diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 3637e7369..14d8236d8 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -33,7 +33,6 @@ * limitations under the License. */ -#include #include #include "BuildConfig.h" @@ -51,6 +50,7 @@ Config::Config() LAUNCHER_GIT = "@Launcher_Git@"; LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; + LAUNCHER_ENVNAME = "@Launcher_ENVName@"; USER_AGENT = "@Launcher_UserAgent@"; @@ -106,13 +106,14 @@ Config::Config() NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; + WIKI_URL = "@Launcher_WIKI_URL@"; HELP_URL = "@Launcher_HELP_URL@"; LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; - FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; + LEGACY_FMLLIBS_BASE_URL = "@Launcher_LEGACY_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 045d987d4..a5851bfba 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -54,6 +54,7 @@ class Config { QString LAUNCHER_GIT; QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; + QString LAUNCHER_ENVNAME; /// The major version number. int VERSION_MAJOR; @@ -128,7 +129,12 @@ class Config { QString NEWS_OPEN_URL; /** - * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help + * URL that gets opened when the user clicks 'Launcher Help' + */ + QString WIKI_URL; + + /** + * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window */ QString HELP_URL; @@ -169,10 +175,10 @@ class Config { QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString FMLLIBS_BASE_URL; + QString LEGACY_FMLLIBS_BASE_URL; QString TRANSLATION_FILES_URL; - QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; + QString FTB_API_BASE_URL = "https://api.feed-the-beast.com/v1/modpacks/public"; QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; @@ -188,8 +194,10 @@ class Config { QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; + QString MODRINTH_DOWNLOAD_HOST = "cdn.modrinth.com"; QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; + QString FLAME_DOWNLOAD_HOST = "edge.forgecdn.net"; QString versionString() const; /** diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake deleted file mode 100644 index 51d2fb13a..000000000 --- a/cmake/CompilerWarnings.cmake +++ /dev/null @@ -1,163 +0,0 @@ -# -# Function to set compiler warnings with reasonable defaults at the project level. -# Taken from https://github.com/aminya/project_options/blob/main/src/CompilerWarnings.cmake -# under the folowing license: -# -# MIT License -# -# Copyright (c) 2022-2100 Amin Yahyaabadi -# -# 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. -# - -include_guard() - -function(_set_project_warnings_add_target_link_option TARGET OPTIONS) - target_link_options(${_project_name} INTERFACE ${OPTIONS}) -endfunction() - -# Set the compiler warnings -# -# https://clang.llvm.org/docs/DiagnosticsReference.html -# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md -function( - set_project_warnings - _project_name - MSVC_WARNINGS - CLANG_WARNINGS - GCC_WARNINGS -) - if("${MSVC_WARNINGS}" STREQUAL "") - set(MSVC_WARNINGS - /W4 # Baseline reasonable warnings - /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data - /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data - /w14263 # 'function': member function does not override any base class virtual member function - /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not - # be destructed correctly - /w14287 # 'operator': unsigned/negative constant mismatch - /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside - # the for-loop scope - /w14296 # 'operator': expression is always 'boolean_value' - /w14311 # 'variable': pointer truncation from 'type1' to 'type2' - /w14545 # expression before comma evaluates to a function which is missing an argument list - /w14546 # function call before comma missing argument list - /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect - /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? - /w14555 # expression has no effect; expected expression with side- effect - /w14619 # pragma warning: there is no warning number 'number' - /w14640 # Enable warning on thread un-safe static member initialization - /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. - /w14905 # wide string literal cast to 'LPSTR' - /w14906 # string literal cast to 'LPWSTR' - /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied - /permissive- # standards conformance mode for MSVC compiler. - - /we4062 # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if("${CLANG_WARNINGS}" STREQUAL "") - set(CLANG_WARNINGS - -Wall - -Wextra # reasonable and standard - -Wshadow # warn the user if a variable declaration shadows one from a parent context - -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps - # catch hard to track down memory errors - -Wold-style-cast # warn for c-style casts - -Wcast-align # warn for potential performance problem casts - -Wunused # warn on anything being unused - -Woverloaded-virtual # warn if you overload (not override) a virtual function - -Wpedantic # warn if non-standard C++ is used - -Wconversion # warn on type conversions that may lose data - -Wsign-conversion # warn on sign conversions - -Wnull-dereference # warn if a null dereference is detected - -Wdouble-promotion # warn if float is implicit promoted to double - -Wformat=2 # warn on security issues around functions that format output (ie printf) - -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation - # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results - # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour - # instead of the exact standard wording so we can safely ignore it - -Wno-gnu-zero-variadic-macro-arguments - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if("${GCC_WARNINGS}" STREQUAL "") - set(GCC_WARNINGS - ${CLANG_WARNINGS} - -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist - -Wduplicated-cond # warn if if / else chain has duplicated conditions - -Wduplicated-branches # warn if if / else branches have duplicated code - -Wlogical-op # warn about logical operations being used where bitwise were probably wanted - -Wuseless-cast # warn if you perform a cast to the same type - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if(MSVC) - set(PROJECT_WARNINGS_CXX ${MSVC_WARNINGS}) - elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") - set(PROJECT_WARNINGS_CXX ${CLANG_WARNINGS}) - elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(PROJECT_WARNINGS_CXX ${GCC_WARNINGS}) - else() - message(AUTHOR_WARNING "No compiler warnings set for CXX compiler: '${CMAKE_CXX_COMPILER_ID}'") - # TODO support Intel compiler - endif() - - # Add C warnings - set(PROJECT_WARNINGS_C "${PROJECT_WARNINGS_CXX}") - list( - REMOVE_ITEM - PROJECT_WARNINGS_C - -Wnon-virtual-dtor - -Wold-style-cast - -Woverloaded-virtual - -Wuseless-cast - -Wextra-semi - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - - target_compile_options( - ${_project_name} - INTERFACE # C++ warnings - $<$:${PROJECT_WARNINGS_CXX}> - # C warnings - $<$:${PROJECT_WARNINGS_C}> - ) - - # If we are using the compiler as a linker driver pass the warnings to it - # (most useful when using LTO or warnings as errors) - if(CMAKE_CXX_LINK_EXECUTABLE MATCHES "^") - _set_project_warnings_add_target_link_option( - ${_project_name} "$<$:${PROJECT_WARNINGS_CXX}>" - ) - endif() - - if(CMAKE_C_LINK_EXECUTABLE MATCHES "^") - _set_project_warnings_add_target_link_option( - ${_project_name} "$<$:${PROJECT_WARNINGS_C}>" - ) - endif() - - endfunction() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index cfd671d68..eb40bacfd 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -7,7 +7,7 @@ NSMicrophoneUsageDescription A Minecraft mod wants to access your microphone. NSDownloadsFolderUsageDescription - Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears. + ${Launcher_DisplayName} 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 ${Launcher_DisplayName} 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 @@ -44,6 +44,8 @@ LSRequiresCarbon + LSApplicationCategoryType + public.app-category.games NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} SUPublicEDKey @@ -59,7 +61,7 @@ mrpack CFBundleTypeName - Prism Launcher instance + ${Launcher_DisplayName} instance CFBundleTypeOSTypes TEXT @@ -85,10 +87,11 @@ CFBundleURLName - Prismlauncher + ${Launcher_Name} CFBundleURLSchemes prismlauncher + ${MACOSX_BUNDLE_EXECUTABLE_NAME} diff --git a/flake.lock b/flake.lock index 730f36a14..640d2bcf1 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "libnbtplusplus": { "flake": false, "locked": { - "lastModified": 1744811532, - "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=", + "lastModified": 1772016279, + "narHash": "sha256-7itkptyjoRcXfGLwg1/jxajetZ3a4mDc66+w4X6yW8s=", "owner": "PrismLauncher", "repo": "libnbtplusplus", - "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78", + "rev": "687e43031df0dc641984b4256bcca50d5b3f7de3", "type": "github" }, "original": { @@ -18,15 +18,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766473571, - "narHash": "sha256-QvjEJNgMVuOootbR+DEfbiW+zSK57U32CE0jmVdcNjQ=", - "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", + "lastModified": 1778443072, + "narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.2403.76701a179d3a/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, "root": { diff --git a/flake.nix b/flake.nix index 6594db522..289e0ec1c 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ }; inputs = { - nixpkgs.url = "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"; + nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; @@ -42,7 +42,7 @@ let pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_19; + llvm = pkgs.llvmPackages_22; in { @@ -85,7 +85,9 @@ let pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_19; + llvm = pkgs.llvmPackages_22; + python = pkgs.python3; + mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; }; packages' = self.packages.${system}; @@ -131,18 +133,36 @@ in { - default = pkgs.mkShell { + default = mkShell { name = "prism-launcher"; inputsFrom = [ packages'.prismlauncher-unwrapped ]; - packages = with pkgs; [ - ccache + packages = [ + pkgs.ccache llvm.clang-tools + python # NOTE(@getchoo): Required for run-clang-tidy, etc. + + (pkgs.stdenvNoCC.mkDerivation { + pname = "clang-tidy-diff"; + inherit (llvm.clang) version; + + nativeBuildInputs = [ + pkgs.installShellFiles + python.pkgs.wrapPython + ]; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + postInstall = "installBin ${llvm.libclang.python}/share/clang/clang-tidy-diff.py"; + postFixup = "wrapPythonPrograms"; + }) ]; cmakeBuildType = "Debug"; - cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags; + cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher-unwrapped.cmakeFlags; dontFixCmake = true; shellHook = '' @@ -165,16 +185,24 @@ formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); - overlays.default = final: prev: { - prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { - inherit - libnbtplusplus - self - ; - }; + overlays.default = + final: prev: - prismlauncher = final.callPackage ./nix/wrapper.nix { }; - }; + let + llvm = final.llvmPackages_22 or prev.llvmPackages_22; + in + + { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit (llvm) stdenv; + inherit + libnbtplusplus + self + ; + }; + + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; packages = forAllSystems ( system: diff --git a/flatpak/cmark.yml b/flatpak/cmark.yml deleted file mode 100644 index d5078baab..000000000 --- a/flatpak/cmark.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: cmark -buildsystem: cmake-ninja -builddir: true -config-opts: - - -DCMAKE_TESTS=OFF -sources: - - type: archive - url: https://github.com/commonmark/cmark/archive/0.31.1.tar.gz - sha256: 3da93db5469c30588cfeb283d9d62edfc6ded9eb0edc10a4f5bbfb7d722ea802 - x-checker-data: - type: anitya - project-id: 9159 - stable-only: true - url-template: https://github.com/commonmark/cmark/archive/$version.tar.gz diff --git a/flatpak/flite.json b/flatpak/flite.json deleted file mode 100644 index 1bf280af1..000000000 --- a/flatpak/flite.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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 deleted file mode 100644 index 1652a2f04..000000000 --- a/flatpak/libdecor.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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 deleted file mode 100644 index 3de68e228..000000000 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ /dev/null @@ -1,154 +0,0 @@ -id: org.prismlauncher.PrismLauncher -runtime: org.kde.Platform -runtime-version: '6.10' -sdk: org.kde.Sdk -sdk-extensions: - - org.freedesktop.Sdk.Extension.openjdk17 - -command: prismlauncher -finish-args: - - --share=ipc - - --socket=x11 - - --socket=wayland - - --device=all - - --share=network - - --socket=pulseaudio - # for Discord RPC mods - - --filesystem=xdg-run/app/com.discordapp.Discord:create - # Mod drag&drop - - --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: - - cmark.yml - - tomlplusplus.yml - - # Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31) - - shared-modules/libusb/libusb.json - - # Needed for proper Wayland support - - libdecor.json - - # 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: - JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 - JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac - run-tests: true - sources: - - type: dir - path: ../ - - - name: glfw - buildsystem: cmake-ninja - config-opts: - - -DCMAKE_BUILD_TYPE=RelWithDebInfo - - -DBUILD_SHARED_LIBS:BOOL=ON - - -DGLFW_BUILD_WAYLAND:BOOL=ON - - -DGLFW_BUILD_DOCS:BOOL=OFF - sources: - - type: git - url: https://github.com/glfw/glfw.git - commit: 7b6aead9fb88b3623e3b3725ebb42670cbe4c579 # 3.4 - - type: patch - path: patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch - cleanup: - - /include - - /lib/cmake - - /lib/pkgconfig - - - name: xrandr - buildsystem: autotools - sources: - - type: archive - url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz - sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c - x-checker-data: - type: anitya - project-id: 14957 - stable-only: true - url-template: https://xorg.freedesktop.org/archive/individual/app/xrandr-$version.tar.xz - cleanup: - - /share/man - - /bin/xkeystone - - - name: gamemode - buildsystem: meson - config-opts: - - -Dwith-sd-bus-provider=no-daemon - - -Dwith-examples=false - post-install: - # gamemoderun is installed for users who want to use wrapper commands - # post-install is running inside the build dir, we need it from the source though - - install -Dm755 ../data/gamemoderun -t /app/bin - sources: - - type: archive - dest-filename: gamemode.tar.gz - url: https://api.github.com/repos/FeralInteractive/gamemode/tarball/1.8.2 - sha256: 2886d4ce543c78bd2a364316d5e7fd59ef06b71de63f896b37c6d3dc97658f60 - x-checker-data: - type: json - url: https://api.github.com/repos/FeralInteractive/gamemode/releases/latest - version-query: .tag_name - url-query: .tarball_url - timestamp-query: .published_at - cleanup: - - /include - - /lib/pkgconfig - - /lib/libgamemodeauto.a - - - name: glxinfo - buildsystem: meson - config-opts: - - --bindir=/app/mesa-demos - - -Degl=disabled - - -Dglut=disabled - - -Dosmesa=disabled - - -Dvulkan=disabled - - -Dwayland=disabled - post-install: - - mv -v /app/mesa-demos/glxinfo /app/bin - sources: - - type: archive - url: https://archive.mesa3d.org/demos/mesa-demos-9.0.0.tar.xz - sha256: 3046a3d26a7b051af7ebdd257a5f23bfeb160cad6ed952329cdff1e9f1ed496b - x-checker-data: - type: anitya - project-id: 16781 - stable-only: true - url-template: https://archive.mesa3d.org/demos/mesa-demos-$version.tar.xz - cleanup: - - /include - - /mesa-demos - - /share - modules: - - shared-modules/glu/glu-9.json - - - name: enhance - buildsystem: simple - build-commands: - - install -Dm755 prime-run /app/bin/prime-run - - mv /app/bin/prismlauncher /app/bin/prismrun - - install -Dm755 prismlauncher /app/bin/prismlauncher - sources: - - type: file - path: prime-run - - type: file - path: prismlauncher 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 deleted file mode 100644 index 70cec9981..000000000 --- a/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch +++ /dev/null @@ -1,59 +0,0 @@ -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/prime-run b/flatpak/prime-run deleted file mode 100644 index 946c28dd5..000000000 --- a/flatpak/prime-run +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -export __NV_PRIME_RENDER_OFFLOAD=1 __VK_LAYER_NV_optimus=NVIDIA_only __GLX_VENDOR_LIBRARY_NAME=nvidia -exec "$@" diff --git a/flatpak/prismlauncher b/flatpak/prismlauncher deleted file mode 100644 index 039d890d2..000000000 --- a/flatpak/prismlauncher +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# discord RPC -for i in {0..9}; do - test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; -done - -export PATH="${PATH}${PATH:+:}/usr/lib/extensions/vulkan/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" -export VK_LAYER_PATH="/usr/lib/extensions/vulkan/share/vulkan/implicit_layer.d/" - -exec /app/bin/prismrun "$@" diff --git a/flatpak/shared-modules b/flatpak/shared-modules deleted file mode 160000 index 73f08ed2c..000000000 --- a/flatpak/shared-modules +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be diff --git a/flatpak/tomlplusplus.yml b/flatpak/tomlplusplus.yml deleted file mode 100644 index 0afaf6678..000000000 --- a/flatpak/tomlplusplus.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: tomlplusplus -buildsystem: cmake-ninja -sources: - - type: archive - url: https://github.com/marzer/tomlplusplus/archive/v3.4.0.tar.gz - sha256: 8517f65938a4faae9ccf8ebb36631a38c1cadfb5efa85d9a72e15b9e97d25155 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 67ddb53e8..ddeb30588 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -50,6 +50,7 @@ #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" +#include "ui/ToolTipFilter.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" @@ -125,12 +126,11 @@ #include #include -#include #include "SysInfo.h" #ifdef Q_OS_LINUX #include -#include "MangoHud.h" +#include "LibraryUtils.h" #include "gamemode_client.h" #endif @@ -157,7 +157,6 @@ #endif #include #include -#include "console/WindowsConsole.h" #endif #include "console/Console.h" @@ -291,21 +290,9 @@ std::tuple read_lock_File(const Q Application::Application(int& argc, char** argv) : QApplication(argc, argv) { -#if defined Q_OS_WIN32 - // 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); @@ -331,6 +318,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "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" }, + { "show-window", "Show the main launcher window (useful in combination with --launch)" }, { { "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" } }); // Has to be positional for some OS to handle that properly @@ -346,12 +334,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); if (parser.isSet("offline")) { - m_offline = true; + m_launchOffline = true; m_offlineName = parser.value("offline"); } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); + m_showMainWindow = parser.isSet("show-window"); for (auto url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); @@ -363,7 +352,7 @@ 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_offline) && + if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_launchOffline) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; @@ -405,7 +394,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } else { QDir foo; if (DesktopServices::isSnap()) { - foo = QDir(getenv("SNAP_USER_COMMON")); + foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON")); } else { foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); } @@ -491,7 +480,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } - if (m_offline) { + if (m_launchOffline) { launch.args["offline_enabled"] = "true"; launch.args["offline_name"] = m_offlineName; } @@ -525,12 +514,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage("The launcher data folder is not writable!", - QString("The launcher couldn't create a log file - the data folder is not writable.\n" + QString("The launcher couldn't create a log file - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" - "(%1)\n" + "(%2)\n" "\n" "The launcher cannot continue until you fix this problem.") + .arg(logFile->errorString()) .arg(dataPath)); return; } @@ -588,39 +578,37 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - bool migrated = false; - - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", - "polymc.cfg"); - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", - "multimc.cfg"); + auto migrated = handleDataMigration( + dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", + "polymc.cfg"); + if (!migrated) { + handleDataMigration(dataPath, + FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), + "MultiMC", "multimc.cfg"); + } } { 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"); + 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()) { - qInfo() << "Work dir before adjustment : " << origcwdPath; - qInfo() << "Work dir after adjustment : " << QDir::currentPath(); - qInfo() << "Adjusted by : " << adjustedBy; + qInfo() << "Work dir before adjustment :" << origcwdPath; + qInfo() << "Work dir after adjustment :" << QDir::currentPath(); + qInfo() << "Adjusted by :" << adjustedBy; } else { - qInfo() << "Work dir : " << QDir::currentPath(); + qInfo() << "Work dir :" << QDir::currentPath(); } - qInfo() << "Binary path : " << binPath; - qInfo() << "Application root path : " << m_rootPath; + qInfo() << "Binary path :" << binPath; + qInfo() << "Application root path :" << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { - qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch; + qInfo() << "ID of instance to launch :" << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { qInfo() << "Address of server to join :" << m_serverToJoin; @@ -637,11 +625,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (check.write(payload) == payload.size()) { check.close(); } else { - qWarning() << "Could not write into" << liveCheckFile << "!"; + qWarning() << "Could not write into" << liveCheckFile << "error:" << check.errorString(); check.remove(); // also closes file! } } else { - qWarning() << "Could not open" << liveCheckFile << "for writing!"; + qWarning() << "Could not open" << liveCheckFile << "for writing:" << check.errorString(); } } @@ -684,8 +672,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QFontInfo consoleFontInfo(consoleFont); QString resolvedDefaultMonospace = consoleFontInfo.family(); QFont resolvedFont(resolvedDefaultMonospace); - qDebug() << "Detected default console font:" << resolvedDefaultMonospace - << ", substitutions:" << resolvedFont.substitutions().join(','); + qDebug().nospace() << "Detected default console font: " << resolvedDefaultMonospace + << ", substitutions: " << resolvedFont.substitutions().join(','); m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); m_settings->registerSetting("ConsoleFontSize", defaultSize); @@ -743,8 +731,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); - m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); m_settings->registerSetting("PermGen", 128); + m_settings->registerSetting("LowMemWarning", true); // Java Settings m_settings->registerSetting("JavaPath", ""); @@ -787,6 +776,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false); + m_settings->registerSetting("ShowModIncompat", false); + m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); @@ -827,6 +818,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); + m_settings->registerSetting("NewsGeometry", ""); + m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); @@ -861,25 +854,23 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } } { + auto resetIfInvalid = [this](const Setting* setting) { + if (const QUrl url(setting->get().toString()); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) { + m_settings->reset(setting->id()); + } + }; + // Meta URL - m_settings->registerSetting("MetaURLOverride", ""); - - QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); - - // get rid of invalid meta urls - if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) - m_settings->reset("MetaURLOverride"); + resetIfInvalid(m_settings->registerSetting("MetaURLOverride", "").get()); // Resource URL - m_settings->registerSetting("ResourceURL", BuildConfig.DEFAULT_RESOURCE_BASE); + resetIfInvalid(m_settings->registerSetting({ "ResourceURLOverride", "ResourceURL" }, "").get()); - QUrl resourceUrl(m_settings->get("ResourceURL").toString()); - - // get rid of invalid resource urls - if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) - m_settings->reset("ResourceURL"); + // Legacy FML libs URL + resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); } + m_settings->registerSetting("MetaRefreshOnLaunch", true); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); @@ -899,6 +890,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->set("FlameKeyOverride", flameKey); m_settings->reset("CFKeyOverride"); } + m_settings->registerSetting("FallbackMRBlockedMods", true); m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("UserAgentOverride", ""); @@ -910,7 +902,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Init page provider { - m_globalSettingsProvider = std::make_shared(tr("Settings")); + m_globalSettingsProvider = std::make_unique(tr("Settings")); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); @@ -943,15 +935,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) qInfo() << "<> Network done."; } - // load translations - { - m_translations.reset(new TranslationsModel("translations")); - auto bcp47Name = m_settings->get("Language").toString(); - m_translations->selectLanguage(bcp47Name); - qInfo() << "Your language is" << bcp47Name; - qInfo() << "<> Translations loaded."; - } - // Instance icons { auto setting = APPLICATION->settings()->getSetting("IconsDir"); @@ -987,11 +970,11 @@ 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 = m_settings->get("InstanceDir").toString(); - qInfo() << "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)); + m_instances.reset(new InstanceList(m_settings.get(), instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); qInfo() << "Loading Instances..."; m_instances->loadList(); @@ -1025,24 +1008,30 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); + m_metacache->addBase("feed", QDir("cache/feed").absolutePath()); m_metacache->Load(); qInfo() << "<> Cache initialized."; } - // now we have network, download translation updates - m_translations->downloadIndex(); + // load translations + { + m_translations.reset(new TranslationsModel("translations")); + m_translations->downloadIndex(); + qInfo() << "Your language is" << m_translations->selectedLanguage(); + qInfo() << "<> Translations loaded."; + } // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (auto profiler : m_profilers.values()) { - profiler->registerSettings(m_settings); + profiler->registerSettings(m_settings.get()); } // Create the MCEdit thing... why is this here? { - m_mcedit.reset(new MCEditTool(m_settings)); + m_mcedit.reset(new MCEditTool(m_settings.get())); } #ifdef Q_OS_MACOS @@ -1199,6 +1188,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } } + if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { + installEventFilter(new ToolTipFilter); + } + if (createSetupWizard()) { return; } @@ -1359,8 +1352,11 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); - return; + launch(inst, m_launchOffline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); + + if (!m_showMainWindow) { + return; + } } } if (!m_instanceIdToShowWindowOf.isEmpty()) { @@ -1413,16 +1409,6 @@ Application::~Application() { // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); - -#if defined Q_OS_WIN32 - // Detach from Windows console - if (consoleAttached) { - fclose(stdout); - fclose(stdin); - fclose(stderr); - FreeConsole(); - } -#endif } void Application::messageReceived(const QByteArray& message) @@ -1464,7 +1450,7 @@ void Application::messageReceived(const QByteArray& message) bool offline = received.args["offline_enabled"] == "true"; QString offlineName = received.args["offline_name"]; - InstancePtr instance; + BaseInstance* instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { @@ -1492,23 +1478,23 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, !offline, false, serverObject, accountObject, offlineName); + launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } } -std::shared_ptr Application::translations() +TranslationsModel* Application::translations() { - return m_translations; + return m_translations.get(); } -std::shared_ptr Application::javalist() +JavaInstallList* Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } - return m_javalist; + return m_javalist.get(); } QIcon Application::logo() @@ -1527,9 +1513,8 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, - bool online, - bool demo, +bool Application::launch(BaseInstance* instance, + LaunchMode mode, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse, const QString& offlineName) @@ -1548,8 +1533,7 @@ bool Application::launch(InstancePtr instance, auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); - controller->setOnline(online); - controller->setDemo(demo); + controller->setLaunchMode(mode); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); @@ -1559,9 +1543,7 @@ bool Application::launch(InstancePtr instance, } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } - connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); - connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); - connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); + connect(controller.get(), &LaunchController::finished, this, &Application::controllerFinished); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; @@ -1575,7 +1557,7 @@ bool Application::launch(InstancePtr instance, return false; } -bool Application::kill(InstancePtr instance) +bool Application::kill(BaseInstance* instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; @@ -1584,7 +1566,7 @@ bool Application::kill(InstancePtr instance) QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive - auto controller = extras.controller; + auto& controller = extras.controller; locker.unlock(); if (controller) { return controller->abort(); @@ -1633,7 +1615,7 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -void Application::controllerSucceeded() +void Application::controllerFinished() { auto controller = qobject_cast(sender()); if (!controller) @@ -1641,10 +1623,11 @@ void Application::controllerSucceeded() auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras[id]; + auto& extras = m_instanceExtras.at(id); + const bool wasSuccessful = controller->wasSuccessful(); // on success, do... - if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { + if (wasSuccessful && controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } @@ -1654,29 +1637,8 @@ void Application::controllerSucceeded() // quit when there are no more windows. if (shouldExitNow()) { - m_status = Status::Succeeded; - exit(0); - } -} - -void Application::controllerFailed(const QString& error) -{ - Q_UNUSED(error); - auto controller = qobject_cast(sender()); - if (!controller) - return; - auto id = controller->id(); - QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras[id]; - - // on failure, do... nothing - extras.controller.reset(); - subRunningInstance(); - - // quit when there are no more windows. - if (shouldExitNow()) { - m_status = Status::Failed; - exit(1); + m_status = wasSuccessful ? Succeeded : Failed; + exit(wasSuccessful ? 0 : 1); } } @@ -1733,7 +1695,7 @@ ViewLogWindow* Application::showLogWindow() return m_viewLogWindow; } -InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) +InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString page) { if (!instance) return nullptr; @@ -1845,22 +1807,22 @@ void Application::updateProxySettings(QString proxyTypeStr, QString addr, int po qDebug() << proxyDesc; } -shared_qobject_ptr Application::metacache() +HttpMetaCache* Application::metacache() { - return m_metacache; + return m_metacache.get(); } -shared_qobject_ptr Application::network() +QNetworkAccessManager* Application::network() { - return m_network; + return m_network.get(); } -shared_qobject_ptr Application::metadataIndex() +Meta::Index* Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } - return m_metadataIndex; + return m_metadataIndex.get(); } void Application::updateCapabilities() @@ -1875,7 +1837,7 @@ void Application::updateCapabilities() if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; - if (!MangoHud::getLibraryString().isEmpty()) + if (!LibraryUtils::findMangoHud().isEmpty()) m_capabilities |= SupportsMangoHud; #endif } @@ -1883,8 +1845,8 @@ void Application::updateCapabilities() void Application::detectLibraries() { #ifdef Q_OS_LINUX - m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); - m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); + m_detectedGLFWPath = LibraryUtils::find(BuildConfig.GLFW_LIBRARY_NAME); + m_detectedOpenALPath = LibraryUtils::find(BuildConfig.OPENAL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; #endif } @@ -1991,7 +1953,7 @@ bool Application::handleDataMigration(const QString& currentData, auto setDoNotMigrate = [&nomigratePath] { QFile file(nomigratePath); if (!file.open(QIODevice::WriteOnly)) { - qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; + qWarning() << "setDoNotMigrate failed; Failed to open file" << file.fileName() << "for writing:" << file.errorString(); } }; @@ -2045,7 +2007,7 @@ void Application::triggerUpdateCheck() } } -QUrl Application::normalizeImportUrl(QString const& url) +QUrl Application::normalizeImportUrl(const QString& url) { auto local_file = QFileInfo(url); if (local_file.exists()) { diff --git a/launcher/Application.h b/launcher/Application.h index 0fd733b50..936e13d71 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -37,6 +37,8 @@ #pragma once +#include + #include #include #include @@ -44,12 +46,10 @@ #include #include #include -#include -#include +#include "QObjectPtr.h" -#include "launch/LogModel.h" -#include "minecraft/launch/MinecraftTarget.h" +#include "minecraft/auth/MinecraftAccount.h" class LaunchController; class LocalPeer; @@ -74,6 +74,12 @@ class ITheme; class MCEditTool; class ThemeManager; class IconTheme; +class BaseInstance; + +class LogModel; + +struct MinecraftTarget; +class MinecraftAccount; namespace Meta { class Index; @@ -91,7 +97,6 @@ class Index; #define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) class Application : public QApplication { - // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { StartingUp, Failed, Succeeded, Initialized }; @@ -112,7 +117,7 @@ class Application : public QApplication { bool event(QEvent* event) override; - std::shared_ptr settings() const { return m_settings; } + SettingsObject* settings() const { return m_settings.get(); } qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } @@ -120,21 +125,21 @@ class Application : public QApplication { ThemeManager* themeManager() { return m_themeManager.get(); } - shared_qobject_ptr updater() { return m_updater; } + ExternalUpdater* updater() { return m_updater.get(); } void triggerUpdateCheck(); - std::shared_ptr translations(); + TranslationsModel* translations(); - std::shared_ptr javalist(); + JavaInstallList* javalist(); - std::shared_ptr instances() const { return m_instances; } + InstanceList* instances() const { return m_instances.get(); } - std::shared_ptr icons() const { return m_icons; } + IconList* icons() const { return m_icons.get(); } MCEditTool* mcedit() const { return m_mcedit.get(); } - shared_qobject_ptr accounts() const { return m_accounts; } + AccountList* accounts() const { return m_accounts.get(); } Status status() const { return m_status; } @@ -142,11 +147,11 @@ class Application : public QApplication { void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); - shared_qobject_ptr network(); + QNetworkAccessManager* network(); - shared_qobject_ptr metacache(); + HttpMetaCache* metacache(); - shared_qobject_ptr metadataIndex(); + Meta::Index* metadataIndex(); void updateCapabilities(); @@ -182,7 +187,7 @@ class Application : public QApplication { */ bool openJsonEditor(const QString& filename); - InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); + InstanceWindow* showInstanceWindow(BaseInstance* instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); ViewLogWindow* showLogWindow(); @@ -194,7 +199,7 @@ class Application : public QApplication { bool updaterEnabled(); QString updaterBinaryName(); - QUrl normalizeImportUrl(QString const& url); + QUrl normalizeImportUrl(const QString& url); signals: void updateAllowedChanged(bool status); @@ -209,20 +214,18 @@ class Application : public QApplication { #endif public slots: - bool launch(InstancePtr instance, - bool online = true, - bool demo = false, - MinecraftTarget::Ptr targetToJoin = nullptr, - MinecraftAccountPtr accountToUse = nullptr, + bool launch(BaseInstance* instance, + LaunchMode mode = LaunchMode::Normal, + std::shared_ptr targetToJoin = nullptr, + shared_qobject_ptr accountToUse = nullptr, const QString& offlineName = QString()); - bool kill(InstancePtr instance); + bool kill(BaseInstance* instance); void closeCurrentWindow(); private slots: void on_windowClose(); void messageReceived(const QByteArray& message); - void controllerSucceeded(); - void controllerFailed(const QString& error); + void controllerFinished(); void setupWizardFinished(int status); private: @@ -238,23 +241,27 @@ class Application : public QApplication { void subRunningInstance(); bool shouldExitNow() const; + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; + private: QDateTime m_startTime; - shared_qobject_ptr m_network; + std::unique_ptr m_network; - shared_qobject_ptr m_updater; - shared_qobject_ptr m_accounts; + std::unique_ptr m_updater; + std::unique_ptr m_accounts; - shared_qobject_ptr m_metacache; - shared_qobject_ptr m_metadataIndex; + std::unique_ptr m_metacache; + std::unique_ptr m_metadataIndex; - std::shared_ptr m_settings; - std::shared_ptr m_instances; - std::shared_ptr m_icons; - std::shared_ptr m_javalist; - std::shared_ptr m_translations; - std::shared_ptr m_globalSettingsProvider; + std::unique_ptr m_settings; + std::unique_ptr m_instances; + std::unique_ptr m_icons; + std::unique_ptr m_javalist; + std::unique_ptr m_translations; + std::unique_ptr m_globalSettingsProvider; std::unique_ptr m_mcedit; QSet m_features; std::unique_ptr m_themeManager; @@ -271,15 +278,10 @@ class Application : public QApplication { Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; #endif -#if defined Q_OS_WIN32 - // used on Windows to attach the standard IO streams - bool consoleAttached = false; -#endif - // FIXME: attach to instances instead. struct InstanceXtras { InstanceWindow* window = nullptr; - shared_qobject_ptr controller; + std::unique_ptr controller; }; std::map m_instanceExtras; mutable QMutex m_instanceExtrasMutex; @@ -307,20 +309,17 @@ class Application : public QApplication { QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; - bool m_offline = false; + bool m_launchOffline = false; QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; + bool m_showMainWindow = false; std::unique_ptr logFile; - shared_qobject_ptr logModel; + std::unique_ptr logModel; public: void addQSavePath(QString); void removeQSavePath(QString); bool checkQSavePath(QString); - - private: - QHash m_qsaveResources; - mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/AssertHelpers.h b/launcher/AssertHelpers.h new file mode 100644 index 000000000..0b1cdb742 --- /dev/null +++ b/launcher/AssertHelpers.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Octol1ttle + * + * This 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 + +#if defined(ASSERT_NEVER) +#error ASSERT_NEVER already defined +#else +#define ASSERT_NEVER(cond) (Q_ASSERT((cond) == false), (cond)) +#endif diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index fdbcc11fe..0080cc516 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -45,6 +45,7 @@ #include "Application.h" #include "Json.h" +#include "launch/LaunchTask.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -53,7 +54,7 @@ #include "Commandline.h" #include "FileSystem.h" -int getConsoleMaxLines(SettingsObjectPtr settings) +int getConsoleMaxLines(SettingsObject* settings) { auto lineSetting = settings->getSetting("ConsoleMaxLines"); bool conversionOk = false; @@ -65,14 +66,14 @@ int getConsoleMaxLines(SettingsObjectPtr settings) return maxLines; } -bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) +bool shouldStopOnConsoleOverflow(SettingsObject* settings) { return settings->get("ConsoleOverflowStop").toBool(); } -BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() +BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : QObject() { - m_settings = settings; + m_settings = std::move(settings); m_global_settings = globalSettings; m_rootDir = rootDir; @@ -122,10 +123,13 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); + m_settings->registerSetting("ManagedPackURL", ""); m_settings->registerSetting("Profiler", ""); } +BaseInstance::~BaseInstance() {} + QString BaseInstance::getPreLaunchCommand() { return settings()->get("PreLaunchCommand").toString(); @@ -335,11 +339,11 @@ QString BaseInstance::instanceRoot() const return m_rootDir; } -SettingsObjectPtr BaseInstance::settings() +SettingsObject* BaseInstance::settings() { loadSpecificSettings(); - return m_settings; + return m_settings.get(); } bool BaseInstance::canLaunch() const @@ -467,9 +471,9 @@ QStringList BaseInstance::extraArguments() return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } -shared_qobject_ptr BaseInstance::getLaunchTask() +LaunchTask* BaseInstance::getLaunchTask() { - return m_launchProcess; + return m_launchProcess.get(); } void BaseInstance::updateRuntimeContext() diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index a542b76eb..9280d2e1c 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -64,9 +64,6 @@ class Task; class LaunchTask; class BaseInstance; -// pointer for lazy people -using InstancePtr = std::shared_ptr; - /// Shortcut saving target representations enum class ShortcutTarget { Desktop, Applications, Other }; @@ -78,8 +75,8 @@ struct ShortcutData { }; /// Console settings -int getConsoleMaxLines(SettingsObjectPtr settings); -bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); +int getConsoleMaxLines(SettingsObject* settings); +bool shouldStopOnConsoleOverflow(SettingsObject* settings); /*! * \brief Base class for instances. @@ -89,11 +86,11 @@ bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); * To create a new instance type, create a new class inheriting from this class * and implement the pure virtual functions. */ -class BaseInstance : public QObject, public std::enable_shared_from_this { +class BaseInstance : public QObject { Q_OBJECT protected: /// no-touchy! - BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); public: /* types */ enum class Status { @@ -103,7 +100,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this createUpdateTask() = 0; /// returns a valid launcher (task container) - virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; + virtual LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) - shared_qobject_ptr getLaunchTask(); + LaunchTask* getLaunchTask(); /*! * Create envrironment variables for running the instance @@ -286,7 +283,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this); + void launchTaskChanged(LaunchTask*); void runningStatusChanged(bool running); @@ -310,10 +307,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_this m_settings; // InstanceFlags m_flags; bool m_isRunning = false; - shared_qobject_ptr m_launchProcess; + std::unique_ptr m_launchProcess; QDateTime m_timeStarted; RuntimeContext m_runtimeContext; @@ -323,7 +320,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this; virtual ~BaseVersion() {} /*! diff --git a/launcher/BaseVersionList.h b/launcher/BaseVersionList.h index 673d13562..ba546e955 100644 --- a/launcher/BaseVersionList.h +++ b/launcher/BaseVersionList.h @@ -63,7 +63,7 @@ class BaseVersionList : public QAbstractListModel { * The task returned by this function should reset the model when it's done. * \return A pointer to a task that reloads the version list. */ - virtual Task::Ptr getLoadTask() = 0; + virtual Task::Ptr getLoadTask(bool forceReload = false) = 0; //! Checks whether or not the list is loaded. If this returns false, the list should be // loaded. diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index fce3bb177..7d4430fd2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -75,9 +75,6 @@ set(CORE_SOURCES # RW lock protected map RWStorage.h - # A variable that has an implicit default value and keeps track of changes - DefaultVariable.h - # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms QObjectPtr.h @@ -102,14 +99,17 @@ set(CORE_SOURCES MMCTime.cpp MTPixmapCache.h + + # Assertion helper + AssertHelpers.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) set(CORE_SOURCES ${CORE_SOURCES} - # MangoHud - MangoHud.h - MangoHud.cpp + # LibraryUtils + LibraryUtils.h + LibraryUtils.cpp ) endif() @@ -119,6 +119,7 @@ set(NET_SOURCES net/ChecksumValidator.h net/Download.cpp net/Download.h + net/DummySink.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp @@ -243,15 +244,13 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h - minecraft/auth/steps/XboxProfileStep.cpp - minecraft/auth/steps/XboxProfileStep.h minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp - minecraft/update/FMLLibrariesTask.cpp - minecraft/update/FMLLibrariesTask.h + minecraft/update/LegacyFMLLibrariesTask.cpp + minecraft/update/LegacyFMLLibrariesTask.h minecraft/update/FoldersTask.cpp minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp @@ -261,6 +260,10 @@ set(MINECRAFT_SOURCES minecraft/launch/ClaimAccount.h minecraft/launch/CreateGameFolders.cpp minecraft/launch/CreateGameFolders.h + minecraft/launch/EnsureAvailableMemory.cpp + minecraft/launch/EnsureAvailableMemory.h + minecraft/launch/EnsureOfflineLibraries.cpp + minecraft/launch/EnsureOfflineLibraries.h minecraft/launch/ModMinecraftJar.cpp minecraft/launch/ModMinecraftJar.h minecraft/launch/ExtractNatives.cpp @@ -346,6 +349,7 @@ set(MINECRAFT_SOURCES minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h + minecraft/mod/ShaderPackFolderModel.cpp minecraft/mod/tasks/ResourceFolderLoadTask.h minecraft/mod/tasks/ResourceFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h @@ -525,6 +529,11 @@ set(FTB_SOURCES modplatform/import_ftb/PackInstallTask.cpp modplatform/import_ftb/PackHelpers.h modplatform/import_ftb/PackHelpers.cpp + + modplatform/ftb/FTBPackInstallTask.h + modplatform/ftb/FTBPackInstallTask.cpp + modplatform/ftb/FTBPackManifest.h + modplatform/ftb/FTBPackManifest.cpp ) set(FLAME_SOURCES @@ -790,6 +799,8 @@ SET(LAUNCHER_SOURCES ApplicationMessage.cpp SysInfo.h SysInfo.cpp + HardwareInfo.cpp + HardwareInfo.h # console utils console/Console.h @@ -806,23 +817,6 @@ SET(LAUNCHER_SOURCES KonamiCode.h KonamiCode.cpp - # Bundled resources - resources/backgrounds/backgrounds.qrc - resources/multimc/multimc.qrc - resources/pe_dark/pe_dark.qrc - resources/pe_light/pe_light.qrc - resources/pe_colored/pe_colored.qrc - resources/pe_blue/pe_blue.qrc - resources/breeze_dark/breeze_dark.qrc - resources/breeze_light/breeze_light.qrc - resources/OSX/OSX.qrc - resources/iOS/iOS.qrc - resources/flat/flat.qrc - resources/flat_white/flat_white.qrc - resources/documents/documents.qrc - resources/shaders/shaders.qrc - "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" - # Icons icons/MMCIcon.h icons/MMCIcon.cpp @@ -842,6 +836,8 @@ SET(LAUNCHER_SOURCES ui/InstanceWindow.cpp ui/ViewLogWindow.h ui/ViewLogWindow.cpp + ui/ToolTipFilter.h + ui/ToolTipFilter.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp @@ -890,6 +886,7 @@ SET(LAUNCHER_SOURCES ui/themes/CatPainter.h # Processes + LaunchMode.h LaunchController.h LaunchController.cpp @@ -1000,6 +997,13 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h + ui/pages/modplatform/ftb/FtbFilterModel.cpp + ui/pages/modplatform/ftb/FtbFilterModel.h + ui/pages/modplatform/ftb/FtbListModel.cpp + ui/pages/modplatform/ftb/FtbListModel.h + ui/pages/modplatform/ftb/FtbPage.cpp + ui/pages/modplatform/ftb/FtbPage.h + ui/pages/modplatform/legacy_ftb/Page.cpp ui/pages/modplatform/legacy_ftb/Page.h ui/pages/modplatform/legacy_ftb/ListModel.h @@ -1063,6 +1067,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ImportResourceDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h + ui/dialogs/NetworkJobFailedDialog.cpp + ui/dialogs/NetworkJobFailedDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp @@ -1200,76 +1206,6 @@ if(WIN32) ) endif() -qt_wrap_ui(LAUNCHER_UI - ui/MainWindow.ui - ui/setupwizard/PasteWizardPage.ui - ui/setupwizard/AutoJavaWizardPage.ui - ui/setupwizard/LoginWizardPage.ui - ui/pages/global/AccountListPage.ui - ui/pages/global/JavaPage.ui - ui/pages/global/LauncherPage.ui - ui/pages/global/APIPage.ui - ui/pages/global/ProxyPage.ui - ui/pages/global/ExternalToolsPage.ui - ui/pages/instance/ExternalResourcesPage.ui - ui/pages/instance/NotesPage.ui - ui/pages/instance/LogPage.ui - ui/pages/instance/ServersPage.ui - ui/pages/instance/OtherLogsPage.ui - ui/pages/instance/VersionPage.ui - ui/pages/instance/ManagedPackPage.ui - ui/pages/instance/WorldListPage.ui - ui/pages/instance/ScreenshotsPage.ui - ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui - ui/pages/modplatform/atlauncher/AtlPage.ui - ui/pages/modplatform/CustomPage.ui - ui/pages/modplatform/ResourcePage.ui - ui/pages/modplatform/flame/FlamePage.ui - ui/pages/modplatform/legacy_ftb/Page.ui - ui/pages/modplatform/import_ftb/ImportFTBPage.ui - ui/pages/modplatform/ImportPage.ui - ui/pages/modplatform/OptionalModDialog.ui - ui/pages/modplatform/modrinth/ModrinthPage.ui - ui/pages/modplatform/technic/TechnicPage.ui - ui/widgets/CustomCommands.ui - ui/widgets/EnvironmentVariables.ui - ui/widgets/InfoFrame.ui - ui/widgets/ModFilterWidget.ui - ui/widgets/SubTaskProgressBar.ui - ui/widgets/AppearanceWidget.ui - ui/widgets/MinecraftSettingsWidget.ui - ui/widgets/JavaSettingsWidget.ui - ui/dialogs/CopyInstanceDialog.ui - ui/dialogs/CreateShortcutDialog.ui - ui/dialogs/ProfileSetupDialog.ui - ui/dialogs/ProgressDialog.ui - ui/dialogs/NewInstanceDialog.ui - ui/dialogs/NewComponentDialog.ui - ui/dialogs/NewsDialog.ui - ui/dialogs/ProfileSelectDialog.ui - ui/dialogs/ExportInstanceDialog.ui - ui/dialogs/ExportPackDialog.ui - ui/dialogs/ExportToModListDialog.ui - ui/dialogs/IconPickerDialog.ui - ui/dialogs/ImportResourceDialog.ui - ui/dialogs/MSALoginDialog.ui - ui/dialogs/AboutDialog.ui - ui/dialogs/ReviewMessageBox.ui - ui/dialogs/ScrollMessageBox.ui - ui/dialogs/BlockedModsDialog.ui - ui/dialogs/ChooseProviderDialog.ui - ui/dialogs/skins/SkinManageDialog.ui - ui/dialogs/ChooseOfflineNameDialog.ui -) - -qt_wrap_ui(PRISM_UPDATE_UI - ui/dialogs/UpdateAvailableDialog.ui -) - -if (NOT Apple) - set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) -endif() - qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc @@ -1282,45 +1218,40 @@ qt_add_resources(LAUNCHER_RESOURCES resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc + resources/flat_white/flat_white.qrc resources/documents/documents.qrc resources/shaders/shaders.qrc "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" ) -qt_wrap_ui(PRISMUPDATER_UI - updater/prismupdater/SelectReleaseDialog.ui - ui/widgets/SubTaskProgressBar.ui - ui/dialogs/ProgressDialog.ui -) - ######## Windows resource files ######## if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() -include(CompilerWarnings) - ######## Precompiled Headers ########### -set(PRECOMPILED_HEADERS - include/base.pch.hpp - include/qtcore.pch.hpp - include/qtgui.pch.hpp -) +if(${Launcher_USE_PCH}) + message(STATUS "Using precompiled headers for applicable launcher targets") + set(PRECOMPILED_HEADERS + include/base.pch.hpp + include/qtcore.pch.hpp + include/qtgui.pch.hpp + ) +endif() ####### Targets ######## # Add executable -add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) -set_project_warnings(Launcher_logic - "${Launcher_MSVC_WARNINGS}" - "${Launcher_CLANG_WARNINGS}" - "${Launcher_GCC_WARNINGS}") +add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_RESOURCES}) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) -target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) + +if(${Launcher_USE_PCH}) + target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) +endif() + target_link_libraries(Launcher_logic - systeminfo Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} @@ -1397,10 +1328,12 @@ if(APPLE) endif() endif() -target_link_libraries(Launcher_logic) - add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) -target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) + +if(${Launcher_USE_PCH}) + target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) +endif() + target_link_libraries(${Launcher_Name} Launcher_logic) if(DEFINED Launcher_APP_BINARY_NAME) @@ -1430,12 +1363,15 @@ endif() if(Launcher_BUILD_UPDATER) # Updater - add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) + add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) + + if(${Launcher_USE_PCH}) + target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) + endif() + target_link_libraries(prism_updater_logic ${ZLIB_LIBRARIES} - systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core @@ -1452,7 +1388,10 @@ if(Launcher_BUILD_UPDATER) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) - target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) + + if(${Launcher_USE_PCH}) + target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) + endif() if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") @@ -1477,16 +1416,14 @@ endif() if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) # File link add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) - set_project_warnings(filelink_logic - "${Launcher_MSVC_WARNINGS}" - "${Launcher_CLANG_WARNINGS}" - "${Launcher_GCC_WARNINGS}") target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) + + if(${Launcher_USE_PCH}) + target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) + endif() target_link_libraries(filelink_logic - systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core @@ -1496,9 +1433,11 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) ) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) - target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) - target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) + + if(${Launcher_USE_PCH}) + target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) + endif() # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo @@ -1536,6 +1475,28 @@ if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) endif() +# Set basic compiler warning/error flags for all targets +get_property(Launcher_TARGETS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) +foreach(target ${Launcher_TARGETS}) + message(STATUS "Enabling all warnings as errors for target '${target}'") + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(${target} PRIVATE /W4 /WX /permissive-) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror) + endif() +endforeach() + +# Disable some warnings in main launcher target due to being present in a lot of places. TODO: Fix them. +if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter + target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter +else() + # sfinae-incomplete is a new GCC warning and triggers in Qt headers + # no-unknown-warning-option so that compilers that don't have sfinae-incomplete don't error + target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) + target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) +endif() + #### The bundle mess! #### # Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. @@ -1585,6 +1546,49 @@ if(WIN32 OR (UNIX AND APPLE)) SCRIPT ${QT_DEPLOY_SCRIPT} COMPONENT bundle ) + # FIXME: remove this crap once we stop using msys2 + if(MINGW) + # i've not found a solution better than injecting the config vars like this... + # with install(CODE" for everything everything just breaks + install(CODE " + set(QT_PLUGINS_DIR \"${QT_PLUGINS_DIR}\") + set(QT_LIBS_DIR \"${QT_LIBS_DIR}\") + set(QT_LIBEXECS_DIR \"${QT_LIBEXECS_DIR}\") + set(CMAKE_SYSTEM_LIBRARY_PATH \"${CMAKE_SYSTEM_LIBRARY_PATH}\") + set(CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\") + " + COMPONENT bundle) + + install(CODE [[ + file(GLOB QT_IMAGEFORMAT_DLLS "${QT_PLUGINS_DIR}/imageformats/*.dll") + set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL objdump) + file(GET_RUNTIME_DEPENDENCIES + RESOLVED_DEPENDENCIES_VAR imageformatdeps + LIBRARIES ${QT_IMAGEFORMAT_DLLS} + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_PLUGINS_DIR} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + # FIXME: Why aren't these caught by the below regex??? + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + POST_EXCLUDE_REGEXES + "system32" + ) + foreach(_lib ${imageformatdeps}) + file(INSTALL + DESTINATION ${CMAKE_INSTALL_PREFIX} + TYPE SHARED_LIBRARY + FOLLOW_SYMLINK_CHAIN + FILES ${_lib} + ) + endforeach() + ]] + COMPONENT bundle) + endif() # Add qt.conf - this makes Qt stop looking for things outside the bundle install( @@ -1598,3 +1602,15 @@ if(WIN32 OR (UNIX AND APPLE)) COMPONENT bundle ) endif() + +find_program(CLANG_FORMAT clang-format OPTIONAL) +if(CLANG_FORMAT) + message(STATUS "Creating clang-format target") + add_custom_target( + clang-format + COMMAND ${CLANG_FORMAT} -i --style=file:${CMAKE_SOURCE_DIR}/.clang-format ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${PRISMUPDATER_SOURCES} ${LINKEXE_SOURCES} ${PRECOMPILED_HEADERS} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) +else() + message(WARNING "Unable to find `clang-format`. Not creating custom target") +endif() diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 9677f868e..cab22089e 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -63,7 +63,7 @@ void DataMigrationTask::dryRunFinished() void DataMigrationTask::dryRunAborted() { - emitFailed(tr("Aborted")); + emitAborted(); } void DataMigrationTask::copyFinished() @@ -81,5 +81,5 @@ void DataMigrationTask::copyFinished() void DataMigrationTask::copyAborted() { - emitFailed(tr("Aborted")); + emitAborted(); } diff --git a/launcher/DefaultVariable.h b/launcher/DefaultVariable.h deleted file mode 100644 index b082091c7..000000000 --- a/launcher/DefaultVariable.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -template -class DefaultVariable { - public: - DefaultVariable(const T& value) { defaultValue = value; } - DefaultVariable& operator=(const T& value) - { - currentValue = value; - is_default = currentValue == defaultValue; - is_explicit = true; - return *this; - } - operator const T&() const { return is_default ? defaultValue : currentValue; } - bool isDefault() const { return is_default; } - bool isExplicit() const { return is_explicit; } - - private: - T currentValue; - T defaultValue; - bool is_default = true; - bool is_explicit = false; -}; diff --git a/launcher/FastFileIconProvider.cpp b/launcher/FastFileIconProvider.cpp index f2b6f4425..1dbab27ba 100644 --- a/launcher/FastFileIconProvider.cpp +++ b/launcher/FastFileIconProvider.cpp @@ -44,4 +44,4 @@ QIcon FastFileIconProvider::icon(const QFileInfo& info) const } return QApplication::style()->standardIcon(icon); -} \ No newline at end of file +} diff --git a/launcher/FastFileIconProvider.h b/launcher/FastFileIconProvider.h index 208534044..7799b7879 100644 --- a/launcher/FastFileIconProvider.h +++ b/launcher/FastFileIconProvider.h @@ -23,4 +23,4 @@ class FastFileIconProvider : public QFileIconProvider { public: QIcon icon(const QFileInfo& info) const override; -}; \ No newline at end of file +}; diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index cebe82eda..445c2a881 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -266,7 +266,21 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { - return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); + if (m_ignoreFiles.contains(fileInfo.fileName())) { + return true; + } + + for (const auto& suffix : m_ignoreFilesSuffixes) { + if (fileInfo.fileName().endsWith(suffix)) { + return true; + } + } + + if (m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()))) { + return true; + } + + return false; } bool FileIgnoreProxy::filterFile(const QFileInfo& file) const diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index 5184fc354..0f149ecb6 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -66,6 +66,7 @@ class FileIgnoreProxy : public QSortFilterProxyModel { // list of file names that need to be removed completely from model inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } + inline QStringList& ignoreFilesWithSuffix() { return m_ignoreFilesSuffixes; } // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } @@ -85,5 +86,6 @@ class FileIgnoreProxy : public QSortFilterProxyModel { const QString m_root; SeparatorPrefixTree<'/'> m_blocked; QStringList m_ignoreFiles; + QStringList m_ignoreFilesSuffixes; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 30d0a9c4c..ef56e3e65 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -36,6 +36,7 @@ */ #include "FileSystem.h" +#include #include #include "BuildConfig.h" @@ -282,6 +283,9 @@ bool copyFileAttributes(QString src, QString dst) if (attrs == INVALID_FILE_ATTRIBUTES) return false; return SetFileAttributesW(dst.toStdWString().c_str(), attrs); +#else + Q_UNUSED(src); + Q_UNUSED(dst); #endif return true; } @@ -432,7 +436,7 @@ void create_link::make_link_list(const QString& offset) link_file(src, ""); } else { if (m_debug) - qDebug() << "linking recursively:" << src << "to" << dst << ", max_depth:" << m_max_depth; + qDebug().nospace() << "linking recursively: " << src << " to " << dst << ", max_depth: " << m_max_depth; QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); @@ -592,7 +596,7 @@ void create_link::runPrivileged(const QString& offset) } ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); - connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, gotResults]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, &gotResults]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); @@ -680,6 +684,32 @@ bool deletePath(QString path) return err.value() == 0; } +bool deleteContents(const QString& path) +{ + const QFileInfo info(path); + if (!info.exists()) { + return true; + } + if (!info.isDir()) { + qWarning() << "Attempted to delete contents of non-directory path:" << path; + return false; + } + + bool ret = true; + + for (const auto& entry : fs::directory_iterator(StringUtils::toStdString(path))) { + std::error_code err; + + fs::remove_all(entry.path(), err); + if (err.value() != 0) { + qWarning().nospace() << "Could not delete directory entry " << entry.path() << ": " << QString::fromStdString(err.message()); + ret = false; + } + } + + return ret; +} + bool trash(QString path, QString* pathInTrash) { // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal @@ -793,68 +823,33 @@ QString NormalizePath(QString path) } } -static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; -static const QString BAD_NTFS_CHARS = "<>:\"|?*"; -static const QString BAD_HFS_CHARS = ":"; - -static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; - -QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +namespace { +const QString g_badChars = "<>:\"|?*\r\n!"; +QString removeChars(QString source, QChar replace, const QString& extraChars = "") { - for (int i = 0; i < string.length(); i++) - if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) - string[i] = replaceWith; - return string; -} - -QString RemoveInvalidPathChars(QString path, QChar replaceWith) -{ - QString invalidChars; -#ifdef Q_OS_WIN - invalidChars = BAD_WIN_CHARS; -#endif - - // the null character is ignored in this check as it was not a problem until now - switch (statFS(path).fsType) { - case FilesystemType::FAT: // similar to NTFS - /* fallthrough */ - case FilesystemType::NTFS: - /* fallthrough */ - case FilesystemType::REFS: // similar to NTFS(should be available only on windows) - invalidChars += BAD_NTFS_CHARS; - break; - // case FilesystemType::EXT: - // case FilesystemType::EXT_2_OLD: - // case FilesystemType::EXT_2_3_4: - // case FilesystemType::XFS: - // case FilesystemType::BTRFS: - // case FilesystemType::NFS: - // case FilesystemType::ZFS: - case FilesystemType::APFS: - /* fallthrough */ - case FilesystemType::HFS: - /* fallthrough */ - case FilesystemType::HFSPLUS: - /* fallthrough */ - case FilesystemType::HFSX: - invalidChars += BAD_HFS_CHARS; - break; - // case FilesystemType::FUSEBLK: - // case FilesystemType::F2FS: - // case FilesystemType::UNKNOWN: - default: - break; + auto badChars = g_badChars; + if (!extraChars.isEmpty()) { + badChars += extraChars; } - if (invalidChars.size() != 0) { - for (int i = 0; i < path.length(); i++) { - if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { - path[i] = replaceWith; - } + for (auto& c : source) { + if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) { + c = replace; } } - return path; + return source; +} +} // namespace + +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +{ + return removeChars(std::move(string), replaceWith, "\\/"); +} + +QString RemoveInvalidPathChars(QString string, QChar replaceWith) +{ + return removeChars(std::move(string), replaceWith); } QString DirNameFromString(QString string, QString inDir) @@ -950,7 +945,10 @@ QString createShortcut(QString destination, QString target, QStringList args, QS qWarning() << "Couldn't create directories within application"; return QString(); } - info.open(QIODevice::WriteOnly | QIODevice::Text); + if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Failed to open file" << info.fileName() << "for writing:" << info.errorString(); + return QString(); + } QFile(icon).rename(resources.path() + "/Icon.icns"); @@ -958,7 +956,10 @@ QString createShortcut(QString destination, QString target, QStringList args, QS QString exec = binaryDir.path() + "/Run.command"; QFile f(exec); - f.open(QIODevice::WriteOnly | QIODevice::Text); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); + return QString(); + } QTextStream stream(&f); auto argstring = quoteArgs(args, "\"", "\\\""); @@ -1001,7 +1002,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS destination += ".desktop"; QFile f(destination); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; + qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); return QString(); } QTextStream stream(&f); @@ -1100,17 +1101,17 @@ QString createShortcut(QString destination, QString target, QStringList args, QS hres = ppf->Save(wsz, TRUE); if (FAILED(hres)) { qWarning() << "IPresistFile->Save() failed"; - qWarning() << "hres = " << hres; + qWarning() << "hres =" << hres; } ppf->Release(); } else { qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; - qWarning() << "hres = " << hres; + qWarning() << "hres =" << hres; } psl->Release(); } else { qWarning() << "Failed to create IShellLink instance"; - qWarning() << "hres = " << hres; + qWarning() << "hres =" << hres; } // go away COM, nobody likes you @@ -1399,14 +1400,14 @@ bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, ULONG fs_flags; if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) { ec = std::error_code(GetLastError(), std::system_category()); - qDebug() << "Failed to get Filesystem information for " << src_path.c_str(); + qDebug() << "Failed to get Filesystem information for" << src_path.c_str(); CloseHandle(hSourceFile); return false; } if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { SetLastError(ERROR_NOT_CAPABLE); ec = std::error_code(GetLastError(), std::system_category()); - qWarning() << "Filesystem at " << src_path.c_str() << " does not support reflink"; + qWarning() << "Filesystem at" << src_path.c_str() << "does not support reflink"; CloseHandle(hSourceFile); return false; } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index f2676b147..6d9b01178 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -291,6 +291,13 @@ bool move(const QString& source, const QString& dest); */ bool deletePath(QString path); +/** + * Delete a folder's contents recursively but not the folder itself. + * @param path The path to the folder. + * @return Whether the deletion was completely successful. + */ +bool deleteContents(const QString& path); + bool removeFiles(QStringList listFile); /** diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 29c71c012..201dcd572 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -172,7 +172,8 @@ int inf(QFile* source, std::function handleBlock) assert(ret != Z_STREAM_ERROR); /* state not clobbered */ switch (ret) { case Z_NEED_DICT: - ret = Z_DATA_ERROR; /* and fall through */ + ret = Z_DATA_ERROR; + [[fallthrough]]; case Z_DATA_ERROR: case Z_MEM_ERROR: (void)inflateEnd(&strm); @@ -215,4 +216,4 @@ QString GZip::readGzFileByBlocks(QFile* source, std::function + * + * This 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 "HardwareInfo.h" + +#include +#include + +#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) +namespace { +QString afterColon(QString str) +{ + return str.remove(0, str.indexOf(':') + 2).trimmed(); +} + +template +bool readFromOutput(const char* command, F function) +{ + FILE* file = popen(command, "r"); // NOLINT(*-command-processor) + if (!file) { + qWarning().nospace() << "Could not execute command '" << command << "': " << strerror(errno); + return false; + } + + constexpr size_t bufferSize = 512; + std::array buffer{}; + while (fgets(buffer.data(), bufferSize, file) != nullptr) { + function(buffer.data()); + } + + const int exitCode = pclose(file); + if (exitCode != 0) { + if (exitCode == -1) { + qWarning().nospace() << "Could not close stream for command '" << command << "': " << strerror(errno); + } else { + qWarning().nospace() << "Command '" << command << "' exited with code " << exitCode; + } + + return false; + } + + return true; +} +} // namespace +#endif + +#ifdef Q_OS_WINDOWS +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include +#include + +#include +using Microsoft::WRL::ComPtr; + +QString HardwareInfo::cpuInfo() +{ + const QSettings registry(R"(HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0)", QSettings::NativeFormat); + return registry.value("ProcessorNameString").toString(); +} + +uint64_t HardwareInfo::totalRamMiB() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof status; + + if (GlobalMemoryStatusEx(&status) == TRUE) { + // transforming bytes -> mib + return status.ullTotalPhys / 1024 / 1024; + } + + qWarning() << "Could not get total RAM: GlobalMemoryStatusEx"; + return 0; +} + +uint64_t HardwareInfo::availableRamMiB() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof status; + + if (GlobalMemoryStatusEx(&status) == TRUE) { + // transforming bytes -> mib + return status.ullAvailPhys / 1024 / 1024; + } + + qWarning() << "Could not get available RAM: GlobalMemoryStatusEx"; + return 0; +} + +QStringList HardwareInfo::gpuInfo() +{ + ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) { + qWarning() << "Could not create DXGI factory:" << Qt::hex << hr; + return { "GPU discovery failed: could not create DXGI factory" }; + } + + UINT i = 0; + ComPtr adapter; + QStringList out; + while (factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND) { + DXGI_ADAPTER_DESC desc; + hr = adapter->GetDesc(&desc); + if (SUCCEEDED(hr)) { + out << "GPU: " + QString::fromWCharArray(desc.Description); // NOLINT(*-pro-bounds-array-to-pointer-decay, *-no-array-decay) + } else { + qWarning() << "Could not get DXGI adapter description:" << Qt::hex << hr; + } + + ++i; + } + + return out; +} + +#elif defined(Q_OS_MACOS) +#include "sys/sysctl.h" + +QString HardwareInfo::cpuInfo() +{ + std::array buffer{}; + size_t bufferSize = buffer.size(); + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &bufferSize, nullptr, 0) == 0) { + return { buffer.data() }; + } + + qWarning() << "Could not get CPU model: sysctlbyname"; + return ""; +} + +uint64_t HardwareInfo::totalRamMiB() +{ + uint64_t memsize = 0; + size_t memsizeSize = sizeof memsize; + if (sysctlbyname("hw.memsize", &memsize, &memsizeSize, nullptr, 0) == 0) { + // transforming bytes -> mib + return memsize / 1024 / 1024; + } + + qWarning() << "Could not get total RAM: sysctlbyname"; + return 0; +} + +uint64_t HardwareInfo::availableRamMiB() +{ + return 0; +} + +MacOSHardwareInfo::MemoryPressureLevel MacOSHardwareInfo::memoryPressureLevel() +{ + uint32_t level = 0; + size_t levelSize = sizeof level; + if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &levelSize, nullptr, 0) == 0) { + return static_cast(level); + } + + qWarning() << "Could not get memory pressure level: sysctlbyname"; + return MemoryPressureLevel::Normal; +} + +QString MacOSHardwareInfo::memoryPressureLevelName() +{ + // The names are internal, users refer to levels by their graph colors in Activity Monitor + switch (memoryPressureLevel()) { + case MemoryPressureLevel::Normal: + return "Green"; + case MemoryPressureLevel::Warning: + return "Yellow"; + case MemoryPressureLevel::Critical: + return "Red"; + default: + Q_ASSERT(false); + return ""; + } +} + +QStringList HardwareInfo::gpuInfo() +{ + QStringList out; + const bool success = readFromOutput("system_profiler SPDisplaysDataType", [&](const QString& str) { + // Chipset Model: Intel HD Graphics 620 + if (str.contains("Chipset Model")) { + out << "GPU: " + afterColon(str); + } + }); + if (!success) { + return { "GPU discovery failed: could not read from system_profiler" }; + } + + return out; +} + +#elif defined(Q_OS_LINUX) +#include + +QString HardwareInfo::cpuInfo() +{ + std::ifstream cpuin("/proc/cpuinfo"); + for (std::string line; std::getline(cpuin, line);) { + // model name : AMD Ryzen 7 5800X 8-Core Processor + if (const QString str = QString::fromStdString(line); str.startsWith("model name")) { + return afterColon(str); + } + } + + qWarning() << "Could not get CPU model: /proc/cpuinfo"; + return "unknown"; +} + +namespace { +uint64_t readMemInfo(const QString& searchTarget) +{ + std::ifstream memin("/proc/meminfo"); + for (std::string line; std::getline(memin, line);) { + // MemTotal: 16287480 kB + if (const QString str = QString::fromStdString(line); str.startsWith(searchTarget)) { + bool ok = false; + const uint total = str.simplified().section(' ', 1, 1).toUInt(&ok); + if (!ok) { + qWarning() << "Could not read /proc/meminfo: failed to parse string:" << str; + return 0; + } + + // transforming kib -> mib + return total / 1024; + } + } + + qWarning() << "Could not read /proc/meminfo: search target not found:" << searchTarget; + return 0; +} +} // namespace + +uint64_t HardwareInfo::totalRamMiB() +{ + return readMemInfo("MemTotal"); +} + +uint64_t HardwareInfo::availableRamMiB() +{ + return readMemInfo("MemAvailable"); +} + +QStringList HardwareInfo::gpuInfo() +{ + bool readingGpuInfo = false; + QString gpu; + QString driverInUse = "NONE"; + QString driversAvailable = "NONE"; + QStringList out; + + const bool success = readFromOutput("lspci -k", [&](const QString& str) { + // clang-format off + // 04:00.0 VGA compatible controller: Advanced Micro Devices, Inc. [AMD/ATI] Ellesmere [Radeon RX 470/480/570/570X/580/580X/590] (rev e7) + // Subsystem: Sapphire Technology Limited Radeon RX 580 Pulse 4GB + // Kernel driver in use: amdgpu + // Kernel modules: amdgpu + // clang-format on + if (str.contains("VGA compatible controller") || str.contains("3D controller")) { + readingGpuInfo = true; + } else if (!str.startsWith('\t')) { + if (readingGpuInfo) { + out << QString("GPU: %1 (driver in use: %2; drivers available: %3)").arg(gpu, driverInUse, driversAvailable); + driverInUse = "NONE"; + driversAvailable = "NONE"; + } + readingGpuInfo = false; + } + + if (!readingGpuInfo) { + return; + } + + const QString value = afterColon(str); + if (str.contains("Subsystem")) { + gpu = value; + } + if (str.contains("Kernel driver in use")) { + driverInUse = value; + } + if (str.contains("Kernel modules")) { + driversAvailable = value; + } + }); + if (!success) { + return { "GPU discovery failed: could not read from lspci" }; + } + + return out; +} + +#else + +QString HardwareInfo::cpuInfo() +{ + return "unknown"; +} + +#if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) +#include + +uint64_t HardwareInfo::totalRamMiB() +{ + uint64_t out = 0; + + const bool success = readFromOutput("sysctl hw.physmem", [&](const QString& str) { + const uint64_t mem = str.mid(12).toULong(); + + // transforming kib -> mib + out = mem / 1024; + }); + if (!success) { + qWarning() << "Could not get total RAM: could not read from sysctl"; + return 0; + } + + return out; +} + +#else +uint64_t HardwareInfo::totalRamMiB() +{ + return 0; +} +#endif + +uint64_t HardwareInfo::availableRamMiB() +{ + return 0; +} + +QStringList HardwareInfo::gpuInfo() +{ + return { "GPU discovery failed: not implemented for this OS" }; +} +#endif diff --git a/launcher/HardwareInfo.h b/launcher/HardwareInfo.h new file mode 100644 index 000000000..4efd339b6 --- /dev/null +++ b/launcher/HardwareInfo.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This 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 + +namespace HardwareInfo { +QString cpuInfo(); +uint64_t totalRamMiB(); +uint64_t availableRamMiB(); +QStringList gpuInfo(); +} // namespace HardwareInfo + +#ifdef Q_OS_MACOS +namespace MacOSHardwareInfo { +enum class MemoryPressureLevel : uint8_t { + Normal = 1, + Warning = 2, + Critical = 4, +}; + +MemoryPressureLevel memoryPressureLevel(); +QString memoryPressureLevelName(); +} // namespace MacOSHardwareInfo +#endif \ No newline at end of file diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index 63c200cc4..087b37340 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -189,4 +189,4 @@ void InstanceCopyPrefs::enableDontLinkSaves(bool b) void InstanceCopyPrefs::enableUseClone(bool b) { useClone = b; -} \ No newline at end of file +} diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index eba1a1339..e32cdf095 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -8,7 +8,7 @@ #include "settings/INISettingsObject.h" #include "tasks/Task.h" -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) +InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); @@ -64,7 +64,6 @@ void InstanceCopyTask::executeTask() savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); - savesCopy->followSymlinks(true); (*savesCopy)(true); setProgress(0, savesCopy->totalCopied()); connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); @@ -126,11 +125,11 @@ void InstanceCopyTask::executeTask() return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher); + folderCopy.matcher(m_matcher); folderCopy(true); setProgress(0, folderCopy.totalCopied()); - connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); + connect(&folderCopy, &FS::copy::fileCopied, [this]() { setProgress(m_progress + 1, m_progressTotal); }); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); @@ -147,9 +146,9 @@ void InstanceCopyTask::copyFinished() } // FIXME: shouldn't this be able to report errors? - auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + auto instanceSettings = std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")); - InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); + BaseInstance* inst(new NullInstance(m_globalSettings, std::move(instanceSettings), m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); if (!m_keepPlaytime) { @@ -197,4 +196,4 @@ bool InstanceCopyTask::abort() return true; } return false; -} \ No newline at end of file +} diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index ef4120bc6..a926af8a7 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -15,7 +15,7 @@ class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); + explicit InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. @@ -26,7 +26,7 @@ class InstanceCopyTask : public InstanceTask { private: /* data */ - InstancePtr m_origInstance; + BaseInstance* m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; Filter m_matcher; diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index bd3514798..e58926660 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,7 +2,25 @@ #include #include -#include "FileSystem.h" + +#include "Application.h" +#include "InstanceTask.h" +#include "minecraft/MinecraftLoadAndCheck.h" +#include "tasks/SequentialTask.h" + +bool InstanceCreationTask::abort() +{ + if (!canAbort()) { + return false; + } + + m_abort = true; + if (m_gameFilesTask) { + return m_gameFilesTask->abort(); + } + + return InstanceTask::abort(); +} void InstanceCreationTask::executeTask() { @@ -19,13 +37,15 @@ void InstanceCreationTask::executeTask() return; } - if (!createInstance()) { - if (m_abort) + m_instance = createInstance(); + if (!m_instance) { + if (m_abort) { return; + } qWarning() << "Instance creation failed!"; if (!m_error_message.isEmpty()) { - qWarning() << "Reason: " << m_error_message; + qWarning() << "Reason:" << m_error_message; emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message)); } else { emitFailed(tr("Error while creating new instance.")); @@ -44,9 +64,10 @@ void InstanceCreationTask::executeTask() setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (const QString& path : m_files_to_remove) { - if (!QFile::exists(path)) + for (const QString& path : m_filesToRemove) { + if (!QFile::exists(path)) { continue; + } qDebug() << "Removing" << path; @@ -61,6 +82,61 @@ void InstanceCreationTask::executeTask() return; } } - if (!m_abort) - emitSucceeded(); + + if (!m_abort) { + if (!APPLICATION->settings()->get("DownloadGameFilesDuringInstanceCreation").toBool()) { + emitSucceeded(); + return; + } + setAbortable(true); + setAbortButtonText(tr("Skip")); + qDebug() << "Downloading game files"; + + auto updateTasks = m_instance->createUpdateTask(); + if (updateTasks.isEmpty()) { + emitSucceeded(); + return; + } + auto task = makeShared(); + task->addTask(makeShared(m_instance.get(), Net::Mode::Online)); + for (const auto& t : updateTasks) { + task->addTask(t); + } + connect(task.get(), &Task::finished, this, [this, task] { + if (task->wasSuccessful() || m_abort) { + emitSucceeded(); + } else { + emitFailed(tr("Could not download game files: %1").arg(task->failReason())); + } + }); + propagateFromOther(task.get()); + setDetails(tr("Downloading game files")); + + m_gameFilesTask = task; + m_gameFilesTask->start(); + } +} + +void InstanceCreationTask::scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled) +{ + if (path.isEmpty()) { + return; + } + if (path.startsWith("saves/")) { + if (m_shouldDeleteSaves == ShouldDeleteSaves::NotAsked) { + m_shouldDeleteSaves = askIfShouldDeleteSaves(parent); + } + if (m_shouldDeleteSaves == ShouldDeleteSaves::No) { + return; + } + } + qDebug() << "Scheduling" << path << "for removal"; + m_filesToRemove.append(dir.absoluteFilePath(path)); + if (checkDisabled) { + if (path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_filesToRemove.append(dir.absoluteFilePath(path.chopped(9))); + } else { + m_filesToRemove.append(dir.absoluteFilePath(path + ".disabled")); + } + } } diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 84fb2a145..39acaf8b2 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -2,6 +2,7 @@ #include "BaseVersion.h" #include "InstanceTask.h" +#include "minecraft/MinecraftInstance.h" class InstanceCreationTask : public InstanceTask { Q_OBJECT @@ -9,6 +10,8 @@ class InstanceCreationTask : public InstanceTask { InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; + bool abort() override; + protected: void executeTask() final override; @@ -27,20 +30,24 @@ class InstanceCreationTask : public InstanceTask { /** * Creates a new instance. * - * Returns whether the instance creation was successful (true) or not (false). + * Returns the instance if it was created or nullptr otherwise. */ - virtual bool createInstance() { return false; }; + virtual std::unique_ptr createInstance() { return nullptr; } QString getError() const { return m_error_message; } protected: void setError(const QString& message) { m_error_message = message; }; + void scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled = false); protected: bool m_abort = false; - QStringList m_files_to_remove; + QStringList m_filesToRemove; + ShouldDeleteSaves m_shouldDeleteSaves; private: QString m_error_message; + std::unique_ptr m_instance; + Task::Ptr m_gameFilesTask; }; diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp index 8be0dccac..75fbdb6c6 100644 --- a/launcher/InstanceDirUpdate.cpp +++ b/launcher/InstanceDirUpdate.cpp @@ -42,7 +42,7 @@ #include "InstanceList.h" #include "ui/dialogs/CustomMessageBox.h" -QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent) +QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent) { if (oldName == newName) return QString(); diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h index b92a59c4c..9da49a9a6 100644 --- a/launcher/InstanceDirUpdate.h +++ b/launcher/InstanceDirUpdate.h @@ -37,7 +37,7 @@ #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); +QString askToUpdateInstanceDirName(BaseInstance* 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 6bc17e058..9b04f99b6 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -150,22 +150,15 @@ void InstanceImportTask::processZipPack() extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; stop = true; - } else { - QFileInfo fileInfo(fileName); - if (fileInfo.fileName() == "instance.cfg") { - qDebug() << "MultiMC:" << true; - m_modpackType = ModpackType::MultiMC; - root = cleanPath(fileInfo.path()); - stop = true; - return true; - } - if (fileInfo.fileName() == "manifest.json") { - qDebug() << "Flame:" << true; - m_modpackType = ModpackType::Flame; - root = cleanPath(fileInfo.path()); - stop = true; - return true; - } + } else if (fileName == "manifest.json") { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + stop = true; + } else if (QFileInfo fileInfo(fileName); fileInfo.fileName() == "instance.cfg") { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + root = cleanPath(fileInfo.path()); + stop = true; } QCoreApplication::processEvents(); return true; @@ -329,6 +322,7 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); @@ -348,9 +342,9 @@ void InstanceImportTask::processTechnic() void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(configPath); + auto instanceSettings = std::make_unique(configPath); - NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + NullInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); // reset time played on import... because packs. instance.resetTimePlayed(); @@ -428,6 +422,7 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index de94db7c3..1339499c7 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -34,42 +34,37 @@ * limitations under the License. */ +#include "InstanceList.h" + #include -#include #include #include #include -#include #include #include #include -#include #include #include -#include -#include #include #include -#include #include "BaseInstance.h" #include "ExponentialSeries.h" #include "FileSystem.h" -#include "InstanceList.h" + #include "InstanceTask.h" #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ShortcutUtils.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 -#include +#include #endif const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) +InstanceList::InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); @@ -143,7 +138,7 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const QStringList InstanceList::getLinkedInstancesById(const QString& id) const { QStringList linkedInstances; - for (auto inst : m_instances) { + for (auto& inst : m_instances) { if (inst->isLinkedToInstanceId(id)) linkedInstances.append(inst->id()); } @@ -153,15 +148,15 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return m_instances.count(); + return count(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); - if (row < 0 || row >= m_instances.size()) + if (row < 0 || row >= count()) return QModelIndex(); - return createIndex(row, column, (void*)m_instances.at(row).get()); + return createIndex(row, column, m_instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const @@ -266,7 +261,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) if (changed) { increaseGroupCount(name); - auto idx = getInstIndex(inst.get()); + auto idx = getInstIndex(inst); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } @@ -457,7 +452,7 @@ void InstanceList::deleteInstance(const InstanceId& id) } } -static QMap getIdMapping(const QList& list) +static QMap getIdMapping(const std::vector>& list) { QMap out; int i = 0; @@ -466,7 +461,7 @@ static QMap getIdMapping(const QList& if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } - out[id] = std::make_pair(item, i); + out[id] = std::make_pair(item.get(), i); i++; } return out; @@ -504,17 +499,16 @@ InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); - QList newList; + std::vector> newList; for (auto& id : discoverInstances()) { if (existingIds.contains(id)) { - auto instPair = existingIds[id]; existingIds.remove(id); qInfo() << "Should keep and soft-reload" << id; } else { - InstancePtr instPtr = loadInstance(id); + std::unique_ptr instPtr = loadInstance(id); if (instPtr) { - newList.append(instPtr); + newList.push_back(std::move(instPtr)); } } } @@ -566,8 +560,8 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for (auto const& itr : m_instances) { - totalPlayTime += itr.get()->totalTimePlayed(); + for (const auto& itr : m_instances) { + totalPlayTime += itr->totalTimePlayed(); } } @@ -578,12 +572,12 @@ void InstanceList::saveNow() } } -void InstanceList::add(const QList& t) +void InstanceList::add(std::vector>& t) { - beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); - m_instances.append(t); + beginInsertRows(QModelIndex(), count(), static_cast(count() + t.size() - 1)); for (auto& ptr : t) { - connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + m_instances.push_back(std::move(ptr)); + connect(m_instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } @@ -613,26 +607,26 @@ void InstanceList::providerUpdated() } } -InstancePtr InstanceList::getInstanceById(QString instId) const +BaseInstance* InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) - return InstancePtr(); + return nullptr; for (auto& inst : m_instances) { if (inst->id() == instId) { - return inst; + return inst.get(); } } - return InstancePtr(); + return nullptr; } -InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const +BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name) const { if (managed_name.isEmpty()) return {}; - for (auto instance : m_instances) { + for (auto& instance : m_instances) { if (instance->getManagedPackName() == managed_name) - return instance; + return instance.get(); } return {}; @@ -640,14 +634,14 @@ InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { - return index(getInstIndex(getInstanceById(id).get())); + return index(getInstIndex(getInstanceById(id))); } int InstanceList::getInstIndex(BaseInstance* inst) const { - int count = m_instances.count(); + int count = this->count(); for (int i = 0; i < count; i++) { - if (inst == m_instances[i].get()) { + if (inst == m_instances.at(i).get()) { return i; } } @@ -663,15 +657,15 @@ void InstanceList::propertiesChanged(BaseInstance* inst) } } -InstancePtr InstanceList::loadInstance(const InstanceId& id) +std::unique_ptr InstanceList::loadInstance(const InstanceId& id) { if (!m_groupsLoaded) { loadGroupList(); } auto instanceRoot = FS::PathCombine(m_instDir, id); - auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); - InstancePtr inst; + auto instanceSettings = std::make_unique(FS::PathCombine(instanceRoot, "instance.cfg")); + std::unique_ptr inst; instanceSettings->registerSetting("InstanceType", ""); @@ -680,9 +674,9 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + inst.reset(new MinecraftInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } else { - inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + inst.reset(new NullInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); @@ -911,20 +905,20 @@ class InstanceStaging : public Task { const unsigned maxBackoff = 16; public: - InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) - : m_parent(parent), backoff(minBackoff, maxBackoff) + InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObject* settings) : m_parent(parent), backoff(minBackoff, maxBackoff) { m_stagingPath = parent->getStagedInstancePath(); m_child.reset(child); m_child->setStagingPath(m_stagingPath); - m_child->setParentSettings(std::move(settings)); + m_child->setParentSettings(settings); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); + connect(child, &Task::abortButtonTextChanged, this, &InstanceStaging::setAbortButtonText); connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::progress, this, &InstanceStaging::setProgress); @@ -932,22 +926,21 @@ class InstanceStaging : public Task { connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } - virtual ~InstanceStaging() {} + ~InstanceStaging() override = default; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if (!canAbort()) + if (!canAbort()) { return false; + } - m_child->abort(); - - return Task::abort(); + return m_child->abort(); } bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: - virtual void executeTask() override + void executeTask() override { if (m_stagingPath.isNull()) { emitFailed(tr("Could not create staging folder")); @@ -962,12 +955,14 @@ class InstanceStaging : public Task { void childSucceeded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { + if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) { + m_backoffTimer.stop(); emitSucceeded(); return; } // we actually failed, retry? if (sleepTime == maxBackoff) { + m_backoffTimer.stop(); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } @@ -976,12 +971,14 @@ class InstanceStaging : public Task { } void childFailed(const QString& reason) { + m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } void childAborted() { + m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitAborted(); } @@ -995,7 +992,7 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - unique_qobject_ptr m_child; + std::unique_ptr m_child; QTimer m_backoffTimer; }; @@ -1028,15 +1025,14 @@ QString InstanceList::getStagedInstancePath() } bool InstanceList::commitStagedInstance(const QString& path, - InstanceName const& instanceName, + const InstanceName& instanceName, QString groupName, - InstanceTask const& commiting) + const InstanceTask& commiting) { if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); QString instID; - InstancePtr inst; auto should_override = commiting.shouldOverride(); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index fc4fa9a39..f0a92d273 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -50,7 +50,7 @@ struct InstanceName; using InstanceId = QString; using GroupId = QString; -using InstanceLocator = std::pair; +using InstanceLocator = std::pair; enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateError, InstExists, CantCreateDir }; @@ -73,7 +73,7 @@ class InstanceList : public QAbstractListModel { Q_OBJECT public: - explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); + explicit InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: @@ -96,17 +96,17 @@ class InstanceList : public QAbstractListModel { */ enum InstListError { NoError = 0, UnknownError }; - InstancePtr at(int i) const { return m_instances.at(i); } + BaseInstance* at(int i) const { return m_instances.at(i).get(); } - int count() const { return m_instances.count(); } + int count() const { return static_cast(m_instances.size()); } InstListError loadList(); void saveNow(); /* O(n) */ - InstancePtr getInstanceById(QString id) const; + BaseInstance* getInstanceById(QString id) const; /* O(n) */ - InstancePtr getInstanceByManagedName(const QString& managed_name) const; + BaseInstance* getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString& id) const; QStringList getGroups(); bool isGroupCollapsed(const QString& groupName); @@ -179,11 +179,11 @@ class InstanceList : public QAbstractListModel { void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); - void add(const QList& list); + void add(std::vector>& list); void loadGroupList(); void saveGroupList(); QList discoverInstances(); - InstancePtr loadInstance(const InstanceId& id); + std::unique_ptr loadInstance(const InstanceId& id); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); @@ -192,11 +192,11 @@ class InstanceList : public QAbstractListModel { int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; - QList m_instances; + std::vector> m_instances; // id -> refs QMap m_groupNameCache; - SettingsObjectPtr m_globalSettings; + SettingsObject* m_globalSettings; QString m_instDir; QFileSystemWatcher* m_watcher; // FIXME: this is so inefficient that looking at it is almost painful. diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index c683774d2..134fb8f24 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -21,26 +21,26 @@ class InstancePageProvider : protected QObject, public BasePageProvider { Q_OBJECT public: - explicit InstancePageProvider(InstancePtr parent) { inst = parent; } + explicit InstancePageProvider(BaseInstance* parent) { inst = parent; } virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; values.append(new LogPage(inst)); - std::shared_ptr onesix = std::dynamic_pointer_cast(inst); - values.append(new VersionPage(onesix.get())); - values.append(ManagedPackPage::createPage(onesix.get())); - auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); + MinecraftInstance* onesix = dynamic_cast(inst); + values.append(new VersionPage(onesix)); + values.append(ManagedPackPage::createPage(onesix)); + auto modsPage = new ModFolderPage(onesix, onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); values.append(modsPage); - values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); - values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); - values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); - values.append(new GlobalDataPackPage(onesix.get())); - values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); - values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); - values.append(new NotesPage(onesix.get())); + values.append(new CoreModFolderPage(onesix, onesix->coreModList())); + values.append(new NilModFolderPage(onesix, onesix->nilModList())); + values.append(new ResourcePackPage(onesix, onesix->resourcePackList())); + values.append(new GlobalDataPackPage(onesix)); + values.append(new TexturePackPage(onesix, onesix->texturePackList())); + values.append(new ShaderPackPage(onesix, onesix->shaderPackList())); + values.append(new NotesPage(onesix)); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); @@ -52,5 +52,5 @@ class InstancePageProvider : protected QObject, public BasePageProvider { virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: - InstancePtr inst; + BaseInstance* inst; }; diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index be10bbe07..01998a7aa 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,4 +1,5 @@ #include "InstanceTask.h" +#include #include "Application.h" #include "settings/SettingsObject.h" @@ -82,3 +83,13 @@ void InstanceName::setName(InstanceName& other) } InstanceTask::InstanceTask() : Task(), InstanceName() {} + +ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent) +{ + auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Delete Existing Save Files"), + QObject::tr("An earlier version of this mod pack installed save files.\n" + "Would you like to remove those existing saves as part of this update?"), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); + auto result = dialog->exec(); + return result == QMessageBox::Yes ? ShouldDeleteSaves::Yes : ShouldDeleteSaves::No; +} diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 86b4cee68..125930a27 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -8,6 +8,8 @@ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); enum class ShouldUpdate { Update, SkipUpdating, Cancel }; [[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); +enum class ShouldDeleteSaves { NotAsked, Yes, No }; +[[nodiscard]] ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent); struct InstanceName { public: @@ -35,7 +37,7 @@ class InstanceTask : public Task, public InstanceName { InstanceTask(); ~InstanceTask() override = default; - void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; } + void setParentSettings(SettingsObject* settings) { m_globalSettings = settings; } void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } @@ -60,7 +62,7 @@ class InstanceTask : public Task, public InstanceName { } protected: /* data */ - SettingsObjectPtr m_globalSettings; + SettingsObject* m_globalSettings; QString m_instIcon; QString m_instGroup; QString m_stagingPath; diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 688f9dae7..2d3372e2e 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -303,7 +303,7 @@ QStringList toStringList(const QString& jsonString) return {}; try { return requireIsArrayOf(doc); - } catch (Json::JsonException& e) { + } catch (Json::JsonException&) { return {}; } } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index cbea045fc..2b882338c 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -40,22 +40,18 @@ #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" +#include "net/NetUtils.h" #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" -#include -#include #include -#include #include #include -#include -#include +#include #include "BuildConfig.h" #include "JavaCommon.h" @@ -63,7 +59,7 @@ #include "tasks/Task.h" #include "ui/dialogs/ChooseOfflineNameDialog.h" -LaunchController::LaunchController() : Task() {} +LaunchController::LaunchController() = default; void LaunchController::executeTask() { @@ -86,9 +82,17 @@ void LaunchController::decideAccount() return; } - // Find an account to use. - auto accounts = APPLICATION->accounts(); - if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) { + // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used + auto* accounts = APPLICATION->accounts(); + const auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); + const auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); + if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { + m_accountToUse = accounts->defaultAccount(); + } else { + m_accountToUse = accounts->at(instanceAccountIndex); + } + + if (!accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " @@ -106,16 +110,7 @@ void LaunchController::decideAccount() } } - // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used - auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); - auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); - if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { - m_accountToUse = accounts->defaultAccount(); - } else { - m_accountToUse = accounts->at(instanceAccountIndex); - } - - if (!m_accountToUse) { + if (!m_accountToUse && accounts->anyAccountIsValid()) { // If no default account is set, ask the user which one to use. ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); @@ -132,46 +127,146 @@ void LaunchController::decideAccount() } } -bool LaunchController::askPlayDemo() +LaunchDecision LaunchController::decideLaunchMode() +{ + if (!m_accountToUse || m_wantedLaunchMode == LaunchMode::Demo) { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + const auto* accounts = APPLICATION->accounts(); + MinecraftAccountPtr accountToCheck = nullptr; + + if (m_accountToUse->accountType() != AccountType::Offline) { + accountToCheck = m_accountToUse->ownsMinecraft() ? m_accountToUse : nullptr; + } else if (const auto defaultAccount = accounts->defaultAccount(); defaultAccount && defaultAccount->ownsMinecraft()) { + accountToCheck = defaultAccount; + } else { + for (int i = 0; i < accounts->count(); i++) { + if (const auto account = accounts->at(i); account->ownsMinecraft()) { + accountToCheck = account; + break; + } + } + } + + if (!accountToCheck) { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + auto state = accountToCheck->accountState(); + const bool needsRefresh = + m_wantedLaunchMode == LaunchMode::Normal && (state == AccountState::Offline || accountToCheck->shouldRefresh()); + if (state == AccountState::Unchecked || state == AccountState::Errored || needsRefresh) { + accountToCheck->refresh(); + state = AccountState::Working; + } + + if (state == AccountState::Working) { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + + // TODO: this relies on tasks' synchronous signal dispatching nature + // TODO: meaning currentTask can't complete and become null while this code is running + // TODO: this code will produce a race condition when tasks become fully async + auto task = accountToCheck->currentTask(); + progDialog.execWithTask(task.get()); + + if (task->getState() == State::AbortedByUser) { + return LaunchDecision::Abort; + } + + state = accountToCheck->accountState(); + } + + QString reauthReason; + switch (state) { + case AccountState::Errored: + reauthReason = tr("An error occurred while refreshing '%1'").arg(accountToCheck->profileName()); + break; + case AccountState::Expired: + reauthReason = tr("'%1' has expired and needs to be reauthenticated").arg(accountToCheck->profileName()); + break; + case AccountState::Disabled: + reauthReason = tr("The launcher's client identification has changed"); + break; + case AccountState::Gone: + reauthReason = tr("'%1' no longer exists on the servers").arg(accountToCheck->profileName()); + break; + default: + m_actualLaunchMode = + state == AccountState::Online && m_wantedLaunchMode == LaunchMode::Normal ? LaunchMode::Normal : LaunchMode::Offline; + return LaunchDecision::Continue; // All good to go + } + + if (reauthenticateAccount(accountToCheck, reauthReason)) { + return LaunchDecision::Undecided; + } + + return LaunchDecision::Abort; +} + +bool LaunchController::askPlayDemo() const { QMessageBox box(m_parentWidget); box.setWindowTitle(tr("Play demo?")); - box.setText( - tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " - "the demo?")); + QString text = m_accountToUse + ? tr("This account does not own Minecraft.\nYou need to purchase the game first to play the full version.") + : tr("No account was selected for launch."); + text += tr("\n\nDo you want to play the demo?"); + box.setText(text); box.setIcon(QMessageBox::Warning); - auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); - auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + const auto* demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto* cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); box.setDefaultButton(cancelButton); box.exec(); return box.clickedButton() == demoButton; } -QString LaunchController::askOfflineName(QString playerName, bool demo, bool* ok) +QString LaunchController::askOfflineName(const QString& playerName, bool* ok) { if (ok != nullptr) { *ok = false; } - // we ask the user for a player name - QString message = tr("Choose your offline mode player name."); - if (demo) { - message = tr("Choose your demo mode player name."); + QString title, message; + title = tr("Player name"); + switch (m_actualLaunchMode) { + case LaunchMode::Normal: + Q_ASSERT(false); + return ""; + case LaunchMode::Demo: + message = tr("Choose your demo mode player name"); + break; + case LaunchMode::Offline: + if (m_wantedLaunchMode == LaunchMode::Normal) { + auto netErr = m_accountToUse->accountData()->networkError; + if (Net::isServerError(netErr)) { + title = tr("Auth servers offline"); + message = tr("The Minecraft authentication servers are currently unavailable, launching in offline mode.\n\n"); + } else { + title = tr("No internet connection"); + message = tr("You are not connected to the Internet, launching in offline mode.\n\n"); + } + } + message += tr("Choose your offline mode player name"); + break; } - QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + const QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; ChooseOfflineNameDialog dialog(message, m_parentWidget); - dialog.setWindowTitle(tr("Player name")); + dialog.setWindowTitle(title); dialog.setUsername(usedname); if (dialog.exec() != QDialog::Accepted) { return {}; } - const QString name = dialog.getUsername(); - usedname = name; + usedname = dialog.getUsername(); APPLICATION->settings()->set("LastOfflinePlayerName", usedname); if (ok != nullptr) { @@ -184,189 +279,79 @@ void LaunchController::login() { decideAccount(); - if (!m_accountToUse) { - // if no account is selected, ask about demo - if (!m_demo) { - m_demo = askPlayDemo(); - } - if (m_demo) { - // we ask the user for a player name + LaunchDecision decision = decideLaunchMode(); + while (decision == LaunchDecision::Undecided) { + decision = decideLaunchMode(); + } + if (decision == LaunchDecision::Abort) { + emitAborted(); + return; + } + + if (m_actualLaunchMode == LaunchMode::Demo) { + if (m_wantedLaunchMode == LaunchMode::Demo || askPlayDemo()) { bool ok = false; - auto name = askOfflineName("Player", m_demo, &ok); + auto name = askOfflineName("Player", &ok); if (ok) { m_session = std::make_shared(); - static const QRegularExpression s_removeChars("[{}-]"); - m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString(QUuid::Id128)); launchInstance(); return; } } - // if no account is selected, we bail - emitFailed(tr("No account selected for launch.")); + + emitFailed(tr("No account selected for launch")); return; } - // we loop until the user succeeds in logging in or gives up - bool tryagain = true; - unsigned int tries = 0; + m_session = std::make_shared(); + m_session->launchMode = m_actualLaunchMode; + m_accountToUse->fillSession(m_session); - if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || - m_accountToUse->shouldRefresh()) { - // Force account refresh on the account used to launch the instance updating the AccountState - // only on first try and if it is not meant to be offline - m_accountToUse->refresh(); - } - while (tryagain) { - if (tries > 0 && tries % 3 == 0) { - auto result = - QMessageBox::question(m_parentWidget, tr("Continue launch?"), - tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?").arg(tries)); - - if (result == QMessageBox::No) { + if (m_accountToUse->accountType() != AccountType::Offline) { + if (m_actualLaunchMode == LaunchMode::Normal && !m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + if (ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); dialog.exec() != QDialog::Accepted) { emitAborted(); return; } } - tries++; - m_session = std::make_shared(); - m_session->wants_online = m_online; - m_session->demo = m_demo; - m_accountToUse->fillSession(m_session); - 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 (accountToCheck->accountState()) { - case AccountState::Offline: { - m_session->wants_online = false; - } - /* fallthrough */ - case AccountState::Online: { - if (!m_session->wants_online) { - // we ask the user for a player name - bool ok = false; - 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 - } 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("Received undetermined session status during login.")); - return; - } + if (m_actualLaunchMode == LaunchMode::Offline && m_accountToUse->accountType() != AccountType::Offline) { + bool ok = false; + QString name = m_offlineName; + if (name.isEmpty()) { + name = askOfflineName(m_session->player_name, &ok); + if (!ok) { + emitAborted(); + 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: { - accountToCheck->refresh(); - } - /* fallthrough */ - case AccountState::Working: { - // refresh is in progress, we need to wait for it to finish to proceed. - ProgressDialog progDialog(m_parentWidget); - progDialog.setSkipButton(true, tr("Abort")); - - auto task = accountToCheck->currentTask(); - progDialog.execWithTask(task.get()); - - // don't retry if aborted - if (task->getState() == Task::State::AbortedByUser) - tryagain = false; - - continue; - } - case AccountState::Expired: { - if (reauthenticateAccount(accountToCheck)) - continue; - return; - } - case AccountState::Disabled: { - 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); - return; - } - case AccountState::Gone: { - auto errorString = - 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); - return; } + m_session->MakeOffline(name); } } - emitFailed(tr("Failed to launch.")); + + launchInstance(); } -bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) +bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason) { 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()), + m_parentWidget, tr("Account refresh failed"), tr("%1. Do you want to reauthenticate this account?").arg(reason), 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()))); + auto* accounts = APPLICATION->accounts(); + const bool isDefault = accounts->defaultAccount() == account; if (account->accountType() == AccountType::MSA) { auto newAccount = MSALoginDialog::newAccount(m_parentWidget); if (newAccount != nullptr) { + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); accounts->addAccount(newAccount); - if (isDefault) + if (isDefault) { accounts->setDefaultAccount(newAccount); + } if (m_accountToUse == account) { m_accountToUse = nullptr; @@ -377,14 +362,13 @@ bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) } } - 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"); - Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); + Q_ASSERT(m_instance != nullptr); + Q_ASSERT(m_session.get() != nullptr); if (!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); @@ -398,37 +382,36 @@ void LaunchController::launchInstance() return; } - auto console = qobject_cast(m_parentWidget); - auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + const auto* console = qobject_cast(m_parentWidget); + const auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); if (!console && showConsole) { APPLICATION->showInstanceWindow(m_instance); } - connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); - connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); - connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); - connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); + connect(m_launcher, &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); + connect(m_launcher, &LaunchTask::succeeded, this, &LaunchController::onSucceeded); + connect(m_launcher, &LaunchTask::failed, this, &LaunchController::onFailed); + connect(m_launcher, &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); // Prepend Online and Auth Status QString online_mode; - if (m_session->wants_online) { + if (m_actualLaunchMode == LaunchMode::Normal) { online_mode = "online"; // Prepend Server Status - QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; + const QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - m_launcher->prependStep(makeShared(m_launcher.get(), servers)); + m_launcher->prependStep(makeShared(m_launcher, servers)); } else { - online_mode = m_demo ? "demo" : "offline"; + online_mode = m_actualLaunchMode == LaunchMode::Demo ? "demo" : "offline"; } - m_launcher->prependStep( - makeShared(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher, "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version { auto versionString = QString("%1 version: %2 (%3)") .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM); - m_launcher->prependStep(makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher, versionString + "\n", MessageLevel::Launcher)); } m_launcher->start(); } @@ -485,10 +468,10 @@ void LaunchController::onFailed(QString reason) if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { APPLICATION->showInstanceWindow(m_instance, "console"); } - emitFailed(reason); + emitFailed(std::move(reason)); } -void LaunchController::onProgressRequested(Task* task) +void LaunchController::onProgressRequested(Task* task) const { ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 50b72eb18..bc0d14e0f 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -36,30 +36,30 @@ #pragma once #include #include -#include #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/launch/MinecraftTarget.h" class InstanceWindow; + +enum class LaunchDecision { Undecided, Continue, Abort }; + class LaunchController : public Task { Q_OBJECT public: void executeTask() override; LaunchController(); - virtual ~LaunchController() = default; + ~LaunchController() override = default; - void setInstance(InstancePtr instance) { m_instance = instance; } + void setInstance(BaseInstance* instance) { m_instance = instance; } - InstancePtr instance() { return m_instance; } + BaseInstance* instance() const { return m_instance; } - void setOnline(bool online) { m_online = online; } + void setLaunchMode(const LaunchMode mode) { m_wantedLaunchMode = mode; } void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } - void setDemo(bool demo) { m_demo = demo; } - void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } void setParentWidget(QWidget* widget) { m_parentWidget = widget; } @@ -68,7 +68,7 @@ class LaunchController : public Task { void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } - QString id() { return m_instance->id(); } + QString id() const { return m_instance->id(); } bool abort() override; @@ -76,27 +76,28 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); - bool askPlayDemo(); - QString askOfflineName(QString playerName, bool demo, bool* ok = nullptr); - bool reauthenticateAccount(MinecraftAccountPtr account); + LaunchDecision decideLaunchMode(); + bool askPlayDemo() const; + QString askOfflineName(const QString& playerName, bool* ok = nullptr); + bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason); private slots: void readyForLaunch(); void onSucceeded(); void onFailed(QString reason); - void onProgressRequested(Task* task); + void onProgressRequested(Task* task) const; private: + LaunchMode m_wantedLaunchMode = LaunchMode::Normal; + LaunchMode m_actualLaunchMode = LaunchMode::Normal; BaseProfilerFactory* m_profiler = nullptr; - bool m_online = true; QString m_offlineName; - bool m_demo = false; - InstancePtr m_instance; + BaseInstance* m_instance = nullptr; QWidget* m_parentWidget = nullptr; InstanceWindow* m_console = nullptr; MinecraftAccountPtr m_accountToUse = nullptr; - AuthSessionPtr m_session; - shared_qobject_ptr m_launcher; - MinecraftTarget::Ptr m_targetToJoin; + AuthSessionPtr m_session = nullptr; + LaunchTask* m_launcher = nullptr; + MinecraftTarget::Ptr m_targetToJoin = nullptr; }; diff --git a/launcher/LaunchMode.h b/launcher/LaunchMode.h new file mode 100644 index 000000000..45cfe50ce --- /dev/null +++ b/launcher/LaunchMode.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This 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 + +enum class LaunchMode { + Normal, + Offline, + Demo, +}; diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 057171a27..28ba32bf8 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -15,12 +15,18 @@ fi LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ +LAUNCHER_ENVNAME=@Launcher_ENVName@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" # Makes the launcher use portals for file picking export QT_QPA_PLATFORMTHEME=xdgdesktopportal +# disable OpenGL and Vulkan launcher features on sharun until https://github.com/VHSgunzo/sharun/issues/35 +if [[ -f "${LAUNCHER_DIR}/sharun" ]]; then + export ${LAUNCHER_ENVNAME}_DISABLE_GLVULKAN=1 +fi + # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" @@ -33,4 +39,4 @@ fi ARGS+=("$@") # Run the launcher -exec -a "${ARGS[@]}" \ No newline at end of file +exec -a "${ARGS[@]}" diff --git a/launcher/MangoHud.cpp b/launcher/LibraryUtils.cpp similarity index 94% rename from launcher/MangoHud.cpp rename to launcher/LibraryUtils.cpp index d85100207..4ac038114 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/LibraryUtils.cpp @@ -25,7 +25,7 @@ #include "FileSystem.h" #include "Json.h" -#include "MangoHud.h" +#include "LibraryUtils.h" #ifdef __GLIBC__ #ifndef _GNU_SOURCE @@ -36,9 +36,9 @@ #include #endif -namespace MangoHud { +namespace LibraryUtils { -QString getLibraryString() +QString findMangoHud() { /** * Guess MangoHud install location by searching for vulkan layers in this order: @@ -123,7 +123,7 @@ QString getLibraryString() #ifdef __GLIBC__ // Check whether mangohud is usable on a glibc based system - QString libraryPath = findLibrary(libraryName); + QString libraryPath = find(libraryName); if (!libraryPath.isEmpty()) { return libraryPath; } @@ -138,7 +138,7 @@ QString getLibraryString() return {}; } -QString findLibrary(QString libName) +QString find(QString libName) { #ifdef __GLIBC__ const char* library = libName.toLocal8Bit().constData(); @@ -161,11 +161,11 @@ QString findLibrary(QString libName) dlclose(handle); return fullPath; #else - qWarning() << "MangoHud::findLibrary is not implemented on this platform"; + qWarning() << "LibraryUtils::find is not implemented on this platform"; return {}; #endif } -} // namespace MangoHud +} // namespace LibraryUtils #ifdef UNDEF_GNU_SOURCE #undef _GNU_SOURCE diff --git a/launcher/MangoHud.h b/launcher/LibraryUtils.h similarity index 87% rename from launcher/MangoHud.h rename to launcher/LibraryUtils.h index 5361999b4..6832a9627 100644 --- a/launcher/MangoHud.h +++ b/launcher/LibraryUtils.h @@ -21,9 +21,9 @@ #include #include -namespace MangoHud { +namespace LibraryUtils { -QString getLibraryString(); +QString findMangoHud(); -QString findLibrary(QString libName); -} // namespace MangoHud +QString find(QString libName); +} // namespace LibraryUtils diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index bae45ad88..c5ef47d7b 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -114,7 +114,7 @@ void LoggedProcess::on_error(QProcess::ProcessError error) { switch (error) { case QProcess::FailedToStart: { - emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); + emit log({ tr("The process failed to start: %1").arg(errorString()) }, MessageLevel::Fatal); changeState(LoggedProcess::FailedToStart); break; } diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 1765fd844..252e6ac57 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -16,7 +16,6 @@ */ #include -#include #include #include @@ -99,4 +98,4 @@ QString Time::humanReadableDuration(double duration, int precision) os.flush(); return outStr; -} \ No newline at end of file +} diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 8e4e433ed..64be89643 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -56,18 +56,18 @@ bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { auto filename = f->filename(); if (filter && !filter(filename)) { - qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; + qDebug() << "Skipping file" << filename << "from" << from.fileName() << "- filtered"; f->skip(); return true; } if (contained.contains(filename)) { - qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); + qDebug() << "Skipping already contained file" << filename << "from" << from.fileName(); f->skip(); return true; } contained.insert(filename); if (!into.addFile(f)) { - qCritical() << "Failed to copy data of " << filename << " into the jar"; + qCritical() << "Failed to copy data of" << filename << "into the jar"; return false; } return true; @@ -149,7 +149,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; return false; } - qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath(); + qDebug() << "Adding folder" << filename.fileName() << "from" << filename.absoluteFilePath(); } else { // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); @@ -233,7 +233,7 @@ std::optional extractSubDir(ArchiveReader* zip, const QString& subd << target; return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; return false; } @@ -321,7 +321,7 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q for (const auto& e : entries) { if (excludeFilter && excludeFilter(e)) { QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); - qDebug() << "Skipping file " << relativeFilePath; + qDebug() << "Skipping file" << relativeFilePath; continue; } diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index 0ba9c5ac8..97db598de 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -14,26 +14,26 @@ else \ type = Qt::DirectConnection; -#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE, RET_DEF) \ static RET_TYPE NAME() \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ return ret; \ } -#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ return ret; \ } -#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE, PARAM_2_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ Q_ARG(PARAM_2_TYPE, p2)); \ @@ -53,18 +53,18 @@ class PixmapCache final : public QObject { static void setInstance(PixmapCache* i) { s_instance = i; } public: - DEFINE_FUNC_NO_PARAM(cacheLimit, int) - DEFINE_FUNC_NO_PARAM(clear, bool) - DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) - DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) - DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) - DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) - DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) - DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) - DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) + DEFINE_FUNC_NO_PARAM(cacheLimit, int, -1) + DEFINE_FUNC_NO_PARAM(clear, bool, false) + DEFINE_FUNC_TWO_PARAM(find, bool, false, const QString&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(find, bool, false, const QPixmapCache::Key&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(insert, bool, false, const QString&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, {}, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QString&) + DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QPixmapCache::Key&) + DEFINE_FUNC_TWO_PARAM(replace, bool, false, const QPixmapCache::Key&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, false, int) + DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool, false) + DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, false, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp index 426067bf6..6f6d34828 100644 --- a/launcher/Markdown.cpp +++ b/launcher/Markdown.cpp @@ -28,4 +28,4 @@ QString markdownToHTML(const QString& markdown) free(buffer); return htmlStr; -} \ No newline at end of file +} diff --git a/launcher/Markdown.h b/launcher/Markdown.h index f91a016bd..57a2e5437 100644 --- a/launcher/Markdown.h +++ b/launcher/Markdown.h @@ -21,4 +21,4 @@ #include #include -QString markdownToHTML(const QString& markdown); \ No newline at end of file +QString markdownToHTML(const QString& markdown); diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 006184780..cff098664 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 13ad0b2c5..1c2425e16 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -41,8 +41,8 @@ class NullInstance : public BaseInstance { Q_OBJECT public: - NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) - : BaseInstance(globalSettings, settings, rootDir) + NullInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) + : BaseInstance(globalSettings, std::move(settings), rootDir) { setVersionBroken(true); } @@ -52,7 +52,7 @@ class NullInstance : public BaseInstance { QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; - shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } + LaunchTask* createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h index ba6154ad8..533195e94 100644 --- a/launcher/PSaveFile.h +++ b/launcher/PSaveFile.h @@ -67,5 +67,5 @@ class PSaveFile : public QSaveFile { QString m_absoluteFilePath; }; #else -#define PSaveFile QSaveFile -#endif \ No newline at end of file +using PSaveFile = QSaveFile; +#endif diff --git a/launcher/QVariantUtils.h b/launcher/QVariantUtils.h index 91f2ad29c..23fe82573 100644 --- a/launcher/QVariantUtils.h +++ b/launcher/QVariantUtils.h @@ -66,4 +66,4 @@ inline QVariant fromList(QList val) return variantList; } -} // namespace QVariantUtils \ No newline at end of file +} // namespace QVariantUtils diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 875a7f02d..bb44d2495 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -19,23 +19,52 @@ #include "ResourceDownloadTask.h" +#include + #include "Application.h" #include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" +namespace { +Net::ModrinthDownloadMeta createModrinthMeta(BaseInstance* instance, QString reason) +{ + auto* mcInstance = dynamic_cast(instance); + if (!mcInstance) { + return {}; + } + + auto* profile = mcInstance->getPackProfile(); + if (!profile) { + return {}; + } + + auto loaders = profile->getModLoadersList(); + + return { + .reason = std::move(reason), + .gameVersion = profile->getComponentVersion("net.minecraft"), + .loader = !loaders.isEmpty() ? ModPlatform::getModLoaderAsString(loaders.first()) : "", + }; +} +} // namespace + ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - const std::shared_ptr packs, - bool is_indexed) + ResourceFolderModel* packs, + bool isIndexed, + QString downloadReason) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) { - if (is_indexed) { + if (isIndexed) { 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); @@ -45,7 +74,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); - auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())); + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()), + Net::Download::Option::NoOptions, + createModrinthMeta(m_pack_model->instance(), std::move(downloadReason))); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: @@ -82,29 +113,31 @@ void ResourceDownloadTask::downloadSucceeded() auto oldName = std::get<0>(to_delete); auto oldFilename = std::get<1>(to_delete); - if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) + if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) { return; + } m_pack_model->uninstallResource(oldFilename, true); // also rename the shader config file - if (dynamic_cast(m_pack_model.get()) != nullptr) { + if (dynamic_cast(m_pack_model) != nullptr) { QFileInfo oldConfig(m_pack_model->dir(), oldFilename + ".txt"); QFileInfo newConfig(m_pack_model->dir(), getFilename() + ".txt"); if (oldConfig.exists() && !newConfig.exists()) { bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); - if (!success) + if (!success) { emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); + } } } } void ResourceDownloadTask::downloadFailed(QString reason) { - emitFailed(reason); m_filesNetJob.reset(); + emitFailed(std::move(reason)); } void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) @@ -114,7 +147,7 @@ void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) // This indirection is done so that we don't delete a mod before being sure it was // downloaded successfully! -void ResourceDownloadTask::hasOldResource(QString name, QString filename) +void ResourceDownloadTask::hasOldResource(const QString& name, const QString& filename) { to_delete = { name, filename }; } diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 78fe8efc7..84324e99a 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -32,8 +32,9 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - std::shared_ptr packs, - bool is_indexed = true); + ResourceFolderModel* packs, + bool isIndexed = true, + QString downloadReason = "standalone"); const QString& getFilename() const { return m_pack_version.fileName; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } @@ -44,7 +45,7 @@ class ResourceDownloadTask : public SequentialTask { private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; - const std::shared_ptr m_pack_model; + ResourceFolderModel* m_pack_model; NetJob::Ptr m_filesNetJob; LocalResourceUpdateTask::Ptr m_update_task; @@ -56,5 +57,5 @@ class ResourceDownloadTask : public SequentialTask { std::tuple to_delete{ "", "" }; private slots: - void hasOldResource(QString name, QString filename); + void hasOldResource(const QString& name, const QString& filename); }; diff --git a/launcher/RuntimeContext.h b/launcher/RuntimeContext.h index 85304a5bc..84a56a892 100644 --- a/launcher/RuntimeContext.h +++ b/launcher/RuntimeContext.h @@ -41,7 +41,7 @@ struct RuntimeContext { return javaRealArchitecture; } - void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) + void updateFromInstanceSettings(SettingsObject* instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index b9e875482..a79cfb5c5 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -35,7 +35,6 @@ */ #include "StringUtils.h" -#include #include #include @@ -232,4 +231,4 @@ QString StringUtils::htmlListPatch(QString htmlStr) 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 cfcf63805..d02b1d854 100644 --- a/launcher/SysInfo.cpp +++ b/launcher/SysInfo.cpp @@ -1,20 +1,54 @@ -#include + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 r58Playz + * Copyright (C) 2024 timoreo + * Copyright (C) 2024 Trial97 + * Copyright (C) 2025 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 -#include "sys.h" -#ifdef Q_OS_MACOS -#include -#endif -#include -#include -#include -#include + +#include "HardwareInfo.h" #ifdef Q_OS_MACOS +#include + bool rosettaDetect() { int ret = 0; size_t size = sizeof(ret); - if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { return false; } return ret == 1; @@ -51,18 +85,13 @@ QString useQTForArch() return QSysInfo::currentCpuArchitecture(); } -int suitableMaxMem() +int defaultMaxJvmMem() { - float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; - int maxMemoryAlloc; - // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB - if (totalRAM < (4096 * 1.5)) - maxMemoryAlloc = (int)(totalRAM / 1.5); + if (const uint64_t totalRAM = HardwareInfo::totalRamMiB(); totalRAM < (4096 * 1.5)) + return totalRAM / 1.5; else - maxMemoryAlloc = 4096; - - return maxMemoryAlloc; + return 4096; } QString getSupportedJavaArchitecture() diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h index f6c04d702..da23c5be1 100644 --- a/launcher/SysInfo.h +++ b/launcher/SysInfo.h @@ -1,9 +1,12 @@ #pragma once + +#include + #include namespace SysInfo { QString currentSystem(); QString useQTForArch(); QString getSupportedJavaArchitecture(); -int suitableMaxMem(); +int defaultMaxJvmMem(); } // namespace SysInfo diff --git a/launcher/Usable.h b/launcher/Usable.h index b0ecd4018..8cef29868 100644 --- a/launcher/Usable.h +++ b/launcher/Usable.h @@ -36,7 +36,7 @@ class Usable { */ class UseLock { public: - UseLock(shared_qobject_ptr usable) : m_usable(usable) + UseLock(Usable* usable) : m_usable(usable) { // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. m_usable->incrementUses(); @@ -44,5 +44,5 @@ class UseLock { ~UseLock() { m_usable->decrementUses(); } private: - shared_qobject_ptr m_usable; + Usable* m_usable; }; diff --git a/launcher/Version.cpp b/launcher/Version.cpp index bffe5d58a..d5496ce1c 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,124 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 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 "Version.h" #include #include #include - -Version::Version(QString str) : m_string(std::move(str)) -{ - parse(); -} - -#define VERSION_OPERATOR(return_on_different) \ - bool exclude_our_sections = false; \ - bool exclude_their_sections = false; \ - \ - const auto size = qMax(m_sections.size(), other.m_sections.size()); \ - for (int i = 0; i < size; ++i) { \ - Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ - Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ - \ - { /* Don't include appendixes in the comparison */ \ - if (sec1.isAppendix()) \ - exclude_our_sections = true; \ - if (sec2.isAppendix()) \ - exclude_their_sections = true; \ - \ - if (exclude_our_sections) { \ - sec1 = Section(); \ - if (sec2.m_isNull) \ - break; \ - } \ - \ - if (exclude_their_sections) { \ - sec2 = Section(); \ - if (sec1.m_isNull) \ - break; \ - } \ - } \ - \ - if (sec1 != sec2) \ - return return_on_different; \ - } - -bool Version::operator<(const Version& other) const -{ - VERSION_OPERATOR(sec1 < sec2) - - return false; -} -bool Version::operator==(const Version& other) const -{ - VERSION_OPERATOR(false) - - return true; -} -bool Version::operator!=(const Version& other) const -{ - return !operator==(other); -} -bool Version::operator<=(const Version& other) const -{ - return *this < other || *this == other; -} -bool Version::operator>(const Version& other) const -{ - return !(*this <= other); -} -bool Version::operator>=(const Version& other) const -{ - return !(*this < other); -} - -void Version::parse() -{ - m_sections.clear(); - QString currentSection; - - if (m_string.isEmpty()) - return; - - auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { - if (lastChar.isNull()) - return false; - if (lastChar.isDigit() != currentChar.isDigit()) - return true; - - const QList s_separators{ '.', '-', '+' }; - if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) - return true; - - return false; - }; - - currentSection += m_string.at(0); - for (int i = 1; i < m_string.size(); ++i) { - const auto& current_char = m_string.at(i); - if (classChange(m_string.at(i - 1), current_char)) { - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); - currentSection = ""; - } - - currentSection += current_char; - } - - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); -} +#include /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) { - QDebugStateSaver saver(debug); + const QDebugStateSaver saver(debug); debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; bool first = true; - for (auto s : v.m_sections) { - if (!first) + for (const auto& s : v.m_sections) { + if (!first) { debug.nospace() << ", "; - debug.nospace() << s.m_fullString; + } + debug.nospace() << s.value; first = false; } @@ -126,3 +44,114 @@ QDebug operator<<(QDebug debug, const Version& v) return debug; } + +std::strong_ordering Version::Section::operator<=>(const Section& other) const +{ + // If both components are numeric, compare numerically (codepoint-wise) + if (this->t == Type::Numeric && other.t == Type::Numeric) { + auto aLen = this->value.size(); + if (aLen != other.value.size()) { + // Lengths differ; compare by length + return aLen <=> other.value.size(); + } + // Compare by digits + auto cmp = QString::compare(this->value, other.value); + if (cmp < 0) { + return std::strong_ordering::less; + } + if (cmp > 0) { + return std::strong_ordering::greater; + } + return std::strong_ordering::equal; + } + // One or both are null + if (this->t == Type::Null) { + if (other.t == Type::PreRelease) { + return std::strong_ordering::greater; + } + return std::strong_ordering::less; + } + if (other.t == Type::Null) { + if (this->t == Type::PreRelease) { + return std::strong_ordering::less; + } + return std::strong_ordering::greater; + } + // Textual comparison (differing type, or both textual/pre-release) + auto minLen = qMin(this->value.size(), other.value.size()); + for (int i = 0; i < minLen; i++) { + auto a = this->value.at(i); + auto b = other.value.at(i); + if (a != b) { + // Compare by rune + return a.unicode() <=> b.unicode(); + } + } + // Compare by length + return this->value.size() <=> other.value.size(); +} + +namespace { +void removeLeadingZeros(QString& s) +{ + s.remove(0, std::distance(s.begin(), std::ranges::find_if_not(s, [](QChar c) { return c == '0'; }))); +} +} // namespace + +void Version::parse() +{ + auto len = m_string.size(); + for (int i = 0; i < len;) { + Section cur(Section::Type::Textual); + auto c = m_string.at(i); + if (c == '+') { + break; // Ignore appendices + } + // custom: the space is special to handle the strings like "1.20 Pre-Release 1" + // this is needed to support Modrinth versions + if (c == '-' || c == ' ') { + // Add dash to component + cur.value += c; + i++; + // If the next rune is non-digit, mark as pre-release (requires >= 1 non-digit after dash so the component has length > 1) + if (i < len && !m_string.at(i).isDigit()) { + cur.t = Section::Type::PreRelease; + } + } else if (c.isDigit()) { + // Mark as numeric + cur.t = Section::Type::Numeric; + } + for (; i < len; i++) { + auto r = m_string.at(i); + if ((r.isDigit() != (cur.t == Section::Type::Numeric)) // starts a new section + || (r == ' ' && cur.t == Section::Type::Numeric) // custom: numeric section then a space is a pre-release + || (r == '-' && cur.t != Section::Type::PreRelease) // "---" is a valid pre-release component + || r == '+') { + // Run completed (do not consume this rune) + break; + } + // Add rune to current run + cur.value += r; + } + if (!cur.value.isEmpty()) { + if (cur.t == Section::Type::Numeric) { + removeLeadingZeros(cur.value); + } + m_sections.append(cur); + } + } +} + +std::strong_ordering Version::operator<=>(const Version& other) const +{ + const auto size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) { + auto sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); + auto sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); + + if (auto cmp = sec1 <=> sec2; cmp != std::strong_ordering::equal) { + return cmp; + } + } + return std::strong_ordering::equal; +} diff --git a/launcher/Version.h b/launcher/Version.h index 4b5ea7119..c0f70f487 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 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 @@ -15,23 +16,6 @@ * * 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 @@ -41,115 +25,36 @@ #include #include -class QUrl; - +// this implements the FlexVer +// https://git.sleeping.town/exa/FlexVer class Version { public: - Version(QString str); + Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions) Version() = default; - bool operator<(const Version& other) const; - bool operator<=(const Version& other) const; - bool operator>(const Version& other) const; - bool operator>=(const Version& other) const; - bool operator==(const Version& other) const; - bool operator!=(const Version& other) const; + private: + struct Section { + enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease }; + explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {} + Type t; + QString value; + bool operator==(const Section& other) const = default; + std::strong_ordering operator<=>(const Section& other) const; + }; + private: + void parse(); + + public: QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); - private: - struct Section { - explicit Section(QString fullString) : m_fullString(std::move(fullString)) - { - qsizetype cutoff = m_fullString.size(); - for (int i = 0; i < m_fullString.size(); i++) { - if (!m_fullString[i].isDigit()) { - cutoff = i; - break; - } - } - - auto numPart = QStringView{ m_fullString }.left(cutoff); - - if (!numPart.isEmpty()) { - m_isNull = false; - m_numPart = numPart.toInt(); - } - - auto stringPart = QStringView{ m_fullString }.mid(cutoff); - - if (!stringPart.isEmpty()) { - m_isNull = false; - m_stringPart = stringPart.toString(); - } - } - - explicit Section() = default; - - bool m_isNull = true; - - int m_numPart = 0; - QString m_stringPart; - - QString m_fullString; - - inline bool isAppendix() const { return m_stringPart.startsWith('+'); } - inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } - - inline bool operator==(const Section& other) const - { - if (m_isNull && !other.m_isNull) - return false; - if (!m_isNull && other.m_isNull) - return false; - - if (!m_isNull && !other.m_isNull) { - return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); - } - - return true; - } - - inline bool operator<(const Section& other) const - { - static auto unequal_is_less = [](Section const& non_null) -> bool { - if (non_null.m_stringPart.isEmpty()) - return non_null.m_numPart == 0; - return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); - }; - - if (!m_isNull && other.m_isNull) - return unequal_is_less(*this); - if (m_isNull && !other.m_isNull) - return !unequal_is_less(other); - - if (!m_isNull && !other.m_isNull) { - if (m_numPart < other.m_numPart) - return true; - if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) - return true; - - if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) - return false; - if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) - return true; - - return false; - } - - return m_fullString < other.m_fullString; - } - - inline bool operator!=(const Section& other) const { return !(*this == other); } - inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } - }; + bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; } + std::strong_ordering operator<=>(const Version& other) const; private: QString m_string; QList
m_sections; - - void parse(); -}; +}; \ No newline at end of file diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 32048db8e..aaab7e8e0 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -198,9 +198,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const return tr("Latest"); } } - } else { - return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } case Qt::DecorationRole: { if (column == Name && hasRecommended) { diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp index d29963c6d..764063de3 100644 --- a/launcher/archive/ArchiveReader.cpp +++ b/launcher/archive/ArchiveReader.cpp @@ -23,7 +23,10 @@ #include #include #include +#include +#include #include +#include namespace MMCZip { QStringList ArchiveReader::getFiles() @@ -34,30 +37,41 @@ QStringList ArchiveReader::getFiles() bool ArchiveReader::collectFiles(bool onlyFiles) { return parse([this, onlyFiles](File* f) { - if (!onlyFiles || f->isFile()) + if (!onlyFiles || f->isFile()) { m_fileNames << f->filename(); + } return f->skip(); }); } +using getPathFunc = std::function; +static QString decodeLibArchivePath(archive_entry* entry, const getPathFunc& getUtf8Path, const getPathFunc& getPath) +{ + auto fileName = QString::fromUtf8(getUtf8Path(entry)); + if (fileName.isEmpty()) { + fileName = QString::fromLocal8Bit(getPath(entry)); + } + return fileName; +} + QString ArchiveReader::File::filename() { - return QString::fromUtf8(archive_entry_pathname_utf8(m_entry)); + return decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname); } QByteArray ArchiveReader::File::readAll(int* outStatus) { QByteArray data; - const void* buff; - size_t size; - la_int64_t offset; + const void* buff = nullptr; + size_t size = 0; + la_int64_t offset = 0; - int status; + int status = 0; while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { data.append(static_cast(buff), static_cast(size)); } if (status != ARCHIVE_EOF && status != ARCHIVE_OK) { - qWarning() << "libarchive read error: " << archive_error_string(m_archive.get()); + qWarning() << "libarchive read error:" << archive_error_string(m_archive.get()); } if (outStatus) { *outStatus = status; @@ -78,10 +92,10 @@ int ArchiveReader::File::readNextHeader() return archive_read_next_header(m_archive.get(), &m_entry); } -auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr +auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr { auto f = std::make_unique(); - auto a = f->m_archive.get(); + auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); @@ -103,15 +117,16 @@ auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) { - int r; - const void* buff; - size_t size; - la_int64_t offset; + int r = 0; + const void* buff = nullptr; + size_t size = 0; + la_int64_t offset = 0; for (;;) { r = archive_read_data_block(ar, &buff, &size, &offset); - if (r == ARCHIVE_EOF) - return (ARCHIVE_OK); + if (r == ARCHIVE_EOF) { + return ARCHIVE_OK; + } if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(ar); return (r); @@ -128,9 +143,43 @@ static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = fal } } -bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) +static bool willEscapeRoot(const QDir& root, archive_entry* entry) { - auto entry = m_entry; + auto entryPath = decodeLibArchivePath(entry, archive_entry_pathname_utf8, archive_entry_pathname); + auto linkTarget = decodeLibArchivePath(entry, archive_entry_symlink_utf8, archive_entry_symlink); + auto hardLink = decodeLibArchivePath(entry, archive_entry_hardlink_utf8, archive_entry_hardlink); + + if (entryPath.isEmpty() || (linkTarget.isEmpty() && hardLink.isEmpty())) { + return false; + } + + bool isHardLink = false; + if (isHardLink = linkTarget.isEmpty(); isHardLink) { + linkTarget = hardLink; + } + + QString linkFullPath = root.filePath(entryPath); + auto rootDir = QUrl::fromLocalFile(root.absolutePath()); + + if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) { + return true; + } + + QDir linkDir = QFileInfo(linkFullPath).dir(); + if (!QDir::isAbsolutePath(linkTarget)) { + linkTarget = (!isHardLink ? linkDir : root).filePath(linkTarget); + } + return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget))); +} + +bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, bool notBlock) +{ + return writeFile(out, targetFileName, {}, notBlock); +}; + +bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock) +{ + auto* entry = m_entry; std::unique_ptr entryClone(nullptr, &archive_entry_free); if (!targetFileName.isEmpty()) { entryClone.reset(archive_entry_clone(m_entry)); @@ -138,26 +187,34 @@ bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool n auto nameUtf8 = targetFileName.toUtf8(); archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); } - if (archive_write_header(out, entry) < ARCHIVE_OK) { - qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); + if (root.has_value() && willEscapeRoot(root.value(), entry)) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root"; return false; - } else if (archive_entry_size(m_entry) > 0) { + } + if (archive_write_header(out, entry) < ARCHIVE_OK) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out) << targetFileName; + return false; + } + if (archive_entry_size(m_entry) > 0) { auto r = copy_data(m_archive.get(), out, notBlock); - if (r < ARCHIVE_OK) + if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(out); - if (r < ARCHIVE_WARN) + } + if (r < ARCHIVE_WARN) { return false; + } } auto r = archive_write_finish_entry(out); - if (r < ARCHIVE_OK) + if (r < ARCHIVE_OK) { qCritical() << "Failed to finish writing entry:" << archive_error_string(out); - return (r > ARCHIVE_WARN); + } + return (r >= ARCHIVE_WARN); } -bool ArchiveReader::parse(std::function doStuff) +bool ArchiveReader::parse(const std::function& doStuff) { auto f = std::make_unique(); - auto a = f->m_archive.get(); + auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); @@ -180,7 +237,8 @@ bool ArchiveReader::parse(std::function doStuff) archive_read_close(a); return true; } -bool ArchiveReader::parse(std::function doStuff) + +bool ArchiveReader::parse(const std::function& doStuff) { return parse([doStuff](File* f, bool&) { return doStuff(f); }); } @@ -204,26 +262,32 @@ QString ArchiveReader::getZipName() bool ArchiveReader::exists(const QString& filePath) const { - if (filePath == QLatin1String("/") || filePath.isEmpty()) + if (filePath == QLatin1String("/") || filePath.isEmpty()) { return true; + } // Normalize input path (remove trailing slash, if any) QString normalizedPath = QDir::cleanPath(filePath); - if (normalizedPath.startsWith('/')) + if (normalizedPath.startsWith('/')) { normalizedPath.remove(0, 1); - if (normalizedPath == QLatin1String(".")) + } + if (normalizedPath == QLatin1String(".")) { return true; - if (normalizedPath == QLatin1String("..")) + } + if (normalizedPath == QLatin1String("..")) { return false; // root only + } // Check for exact file match - if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) + if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) { return true; + } // Check for directory existence by seeing if any file starts with that path QString dirPath = normalizedPath + QLatin1Char('/'); for (const QString& f : m_fileNames) { - if (f.startsWith(dirPath, Qt::CaseInsensitive)) + if (f.startsWith(dirPath, Qt::CaseInsensitive)) { return true; + } } return false; diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h index 379006278..4f11d2e06 100644 --- a/launcher/archive/ArchiveReader.h +++ b/launcher/archive/ArchiveReader.h @@ -19,8 +19,11 @@ #include #include +#include #include #include +#include +#include struct archive; struct archive_entry; @@ -28,7 +31,7 @@ namespace MMCZip { class ArchiveReader { public: using ArchivePtr = std::unique_ptr; - ArchiveReader(QString fileName) : m_archivePath(fileName) {} + explicit ArchiveReader(QString fileName) : m_archivePath(std::move(fileName)) {} virtual ~ArchiveReader() = default; QStringList getFiles(); @@ -48,7 +51,8 @@ class ArchiveReader { QByteArray readAll(int* outStatus = nullptr); bool skip(); - bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); + bool writeFile(archive* out, const QString& targetFileName = "", bool notBlock = false); + bool writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock = false); private: int readNextHeader(); @@ -59,14 +63,14 @@ class ArchiveReader { archive_entry* m_entry; }; - std::unique_ptr goToFile(QString filename); - bool parse(std::function); - bool parse(std::function); + std::unique_ptr goToFile(const QString& filename); + bool parse(const std::function&); + bool parse(const std::function&); private: QString m_archivePath; size_t m_blockSize = 10240; - QStringList m_fileNames = {}; + QStringList m_fileNames; }; -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.cpp b/launcher/archive/ArchiveWriter.cpp index a546f2bb7..43dbe4dbd 100644 --- a/launcher/archive/ArchiveWriter.cpp +++ b/launcher/archive/ArchiveWriter.cpp @@ -167,14 +167,14 @@ bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) } if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { - qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (fileInfo.isFile() && !fileInfo.isSymLink()) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file: " << fileInfo.filePath(); + qCritical() << "Failed to open file:" << fileInfo.filePath() << "error:" << file.errorString(); return false; } @@ -185,12 +185,12 @@ bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) while (!file.atEnd()) { auto bytesRead = file.read(buffer.data(), chunkSize); if (bytesRead < 0) { - qCritical() << "Read error in file: " << fileInfo.filePath(); + qCritical() << "Read error in file:" << fileInfo.filePath(); return false; } if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) { - qCritical() << "Write error in archive for: " << fileDest; + qCritical() << "Write error in archive for:" << fileDest; return false; } } @@ -216,12 +216,12 @@ bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) archive_entry_set_size(entry, data.size()); if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { - qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (archive_write_data(m_archive, data.constData(), data.size()) < 0) { - qCritical() << "Write error in archive for: " << fileDest << "-" << archive_error_string(m_archive); + qCritical() << "Write error in archive for:" << fileDest << "-" << archive_error_string(m_archive); return false; } return true; diff --git a/launcher/archive/ArchiveWriter.h b/launcher/archive/ArchiveWriter.h index 807bb297c..50858b517 100644 --- a/launcher/archive/ArchiveWriter.h +++ b/launcher/archive/ArchiveWriter.h @@ -43,4 +43,4 @@ class ArchiveWriter { QString m_filename; QString m_format = "zip"; }; -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.cpp b/launcher/archive/ExportToZipTask.cpp index 9df9539be..bd3bc9032 100644 --- a/launcher/archive/ExportToZipTask.cpp +++ b/launcher/archive/ExportToZipTask.cpp @@ -97,4 +97,4 @@ bool ExportToZipTask::abort() } return false; } -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.h b/launcher/archive/ExportToZipTask.h index 21ad1db48..0c8329c93 100644 --- a/launcher/archive/ExportToZipTask.h +++ b/launcher/archive/ExportToZipTask.h @@ -69,4 +69,4 @@ class ExportToZipTask : public Task { QFuture m_buildZipFuture; QFutureWatcher m_buildZipWatcher; }; -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp index b958640c9..35dc39d90 100644 --- a/launcher/archive/ExtractZipTask.cpp +++ b/launcher/archive/ExtractZipTask.cpp @@ -95,7 +95,7 @@ auto ExtractZipTask::extractZip() -> ZipResult return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); return false; } @@ -132,4 +132,4 @@ bool ExtractZipTask::abort() return false; } -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.h b/launcher/archive/ExtractZipTask.h index 284d873fe..03c391aee 100644 --- a/launcher/archive/ExtractZipTask.h +++ b/launcher/archive/ExtractZipTask.h @@ -50,4 +50,4 @@ class ExtractZipTask : public Task { QFuture m_zipFuture; QFutureWatcher m_zipWatcher; }; -} // namespace MMCZip \ No newline at end of file +} // namespace MMCZip diff --git a/launcher/console/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp index 4a0eb3d3d..e12183624 100644 --- a/launcher/console/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -24,12 +24,16 @@ #endif #include +#include #include +#include #include #include #include #include +namespace console { + void RedirectHandle(DWORD handle, FILE* stream, const char* mode) { HANDLE stdHandle = GetStdHandle(handle); @@ -157,3 +161,31 @@ std::error_code EnableAnsiSupport() return {}; } + +void FreeWindowsConsole() +{ + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); +} + +WindowsConsoleGuard::WindowsConsoleGuard() : m_consoleAttached(false) +{ + if (console::AttachWindowsConsole()) { + m_consoleAttached = true; + if (auto err = console::EnableAnsiSupport(); err) { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +} + +WindowsConsoleGuard::~WindowsConsoleGuard() +{ + // Detach from Windows console + if (m_consoleAttached) { + console::FreeWindowsConsole(); + } +} + +} // namespace console diff --git a/launcher/console/WindowsConsole.h b/launcher/console/WindowsConsole.h index 4c1f3ee28..52102217d 100644 --- a/launcher/console/WindowsConsole.h +++ b/launcher/console/WindowsConsole.h @@ -21,8 +21,24 @@ #pragma once -#include +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +namespace console { void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); std::error_code EnableAnsiSupport(); +void FreeWindowsConsole(); + +class WindowsConsoleGuard { + public: + WindowsConsoleGuard(); + ~WindowsConsoleGuard(); + + private: + bool m_consoleAttached; +}; + +} // namespace console diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index 1494fa8cc..d87a1078b 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -34,26 +34,11 @@ #include -#include - -#if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include "console/WindowsConsole.h" -#endif - #include namespace fs = std::filesystem; FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { -#if defined Q_OS_WIN32 - // attach the parent console - if (AttachWindowsConsole()) { - consoleAttached = true; - } -#endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); @@ -115,7 +100,7 @@ void FileLinkApp::joinServer(QString server) qDebug() << ("The connection was closed by the peer. "); break; default: - qDebug() << "The following error occurred: " << socket.errorString(); + qDebug() << "The following error occurred:" << socket.errorString(); } }); @@ -236,13 +221,4 @@ FileLinkApp::~FileLinkApp() qDebug() << "link program shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); - -#if defined Q_OS_WIN32 - // Detach from Windows console - if (consoleAttached) { - fclose(stdout); - fclose(stdin); - fclose(stderr); - } -#endif } diff --git a/launcher/filelink/FileLink.h b/launcher/filelink/FileLink.h index 583d0d43a..25fdb71fc 100644 --- a/launcher/filelink/FileLink.h +++ b/launcher/filelink/FileLink.h @@ -38,7 +38,6 @@ #include "FileSystem.h" class FileLinkApp : public QCoreApplication { - // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized }; @@ -64,8 +63,4 @@ class FileLinkApp : public QCoreApplication { QList m_links_to_make; QList m_path_results; -#if defined Q_OS_WIN32 - // used on Windows to attach the standard IO streams - bool consoleAttached = false; -#endif }; diff --git a/launcher/filelink/filelink_main.cpp b/launcher/filelink/filelink_main.cpp index 2a8bcb703..d34844370 100644 --- a/launcher/filelink/filelink_main.cpp +++ b/launcher/filelink/filelink_main.cpp @@ -22,8 +22,17 @@ #include "FileLink.h" +#if defined Q_OS_WIN32 +#include "console/WindowsConsole.h" +#endif + int main(int argc, char* argv[]) { +#if defined Q_OS_WIN32 + // attach the parent console + console::WindowsConsoleGuard _consoleGuard; +#endif + FileLinkApp ldh(argc, argv); switch (ldh.status()) { diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index fb80f89da..ad48665d4 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include "icons/IconUtils.h" @@ -146,8 +147,7 @@ void IconList::directoryChanged(const QString& path) { QDir newDir(path); if (m_dir.absolutePath() != newDir.absolutePath()) { - if (!path.startsWith(m_dir.absolutePath())) - m_dir.setPath(path); + m_dir.setPath(path); m_dir.refresh(); if (m_isWatching) stopWatching(); @@ -169,7 +169,7 @@ void IconList::directoryChanged(const QString& path) QSet toAdd = newSet - currentSet; for (const QString& removedPath : toRemove) { - qDebug() << "Removing icon " << removedPath; + qDebug() << "Removing icon" << removedPath; QFileInfo removedFile(removedPath); QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); QString key = QFileInfo(relativePath).completeBaseName(); @@ -191,7 +191,7 @@ void IconList::directoryChanged(const QString& path) } for (const QString& addedPath : toAdd) { - qDebug() << "Adding icon " << addedPath; + qDebug() << "Adding icon" << addedPath; QFileInfo addfile(addedPath); QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); @@ -209,7 +209,7 @@ void IconList::directoryChanged(const QString& path) void IconList::fileChanged(const QString& path) { - qDebug() << "Checking icon " << path; + qDebug() << "Checking icon" << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; @@ -217,7 +217,13 @@ void IconList::fileChanged(const QString& path) int idx = getIconIndex(key); if (idx == -1) return; - QIcon icon(path); + QIcon icon; + // special handling for jpg and jpeg to go through pixmap to keep the size constant + if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { + icon.addPixmap(QPixmap(path)); + } else { + icon.addFile(path); + } if (icon.availableSizes().empty()) return; @@ -240,9 +246,9 @@ void IconList::startWatching() FS::ensureFolderPathExists(abs_path); m_isWatching = addPathRecursively(abs_path); if (m_isWatching) { - qDebug() << "Started watching " << abs_path; + qDebug() << "Started watching" << abs_path; } else { - qDebug() << "Failed to start watching " << abs_path; + qDebug() << "Failed to start watching" << abs_path; } } @@ -395,7 +401,14 @@ bool IconList::addThemeIcon(const QString& key) bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) { // replace the icon even? is the input valid? - QIcon icon(path); + QIcon icon; + // special handling for jpg and jpeg to go through pixmap to keep the size constant + if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { + icon.addPixmap(QPixmap(path)); + } else { + icon.addFile(path); + } + if (icon.isNull()) return false; auto iter = m_nameIndex.find(key); @@ -474,4 +487,4 @@ QString IconList::iconDirectory(const QString& key) const } } return getDirectory(); -} \ No newline at end of file +} diff --git a/launcher/include/base.pch.hpp b/launcher/include/base.pch.hpp index c858857f9..ecaf41fdd 100644 --- a/launcher/include/base.pch.hpp +++ b/launcher/include/base.pch.hpp @@ -3,6 +3,7 @@ #define PRISM_PRECOMPILED_BASE_HEADERS_H #include +#include #include #include #include @@ -12,6 +13,5 @@ #include #include #include -#include #endif // PRISM_PRECOMPILED_BASE_HEADERS_H diff --git a/launcher/include/qtcore.pch.hpp b/launcher/include/qtcore.pch.hpp index d7c24ddb6..b8836618a 100644 --- a/launcher/include/qtcore.pch.hpp +++ b/launcher/include/qtcore.pch.hpp @@ -5,9 +5,15 @@ #include #include #include + +#include +#include + #include +#include #include +#include #include diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 23bd1b73e..3a04756fa 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -163,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" || os_arch == "riscv64"; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64" || os_arch == "ppc64le" || os_arch == "ppc64"; result.validity = Result::Validity::Valid; result.is_64bit = is_64; @@ -179,13 +179,20 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) void JavaChecker::error(QProcess::ProcessError err) { if (err == QProcess::FailedToStart) { - qDebug() << "Java checker has failed to start."; + qDebug() << "Java checker has failed to start:" << process->errorString(); qDebug() << "Process environment:"; qDebug() << process->environment(); qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); - emit checkFinished({ m_path, m_id }); + + Result result = { + m_path, + m_id, + }; + result.errorLog = process->errorString(); + result.validity = Result::Validity::Errored; + emit checkFinished(result); } emitSucceeded(); } diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 30cb77e08..98aac5cab 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -49,7 +49,7 @@ bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } @@ -58,7 +58,7 @@ bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index d8fd477fd..5899964f0 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -39,7 +39,6 @@ struct JavaInstall : public BaseVersion { JavaVersion id; QString arch; QString path; - bool recommended = false; bool is_64bit = false; }; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index aa7fab8a0..1d7b9cdff 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -41,6 +41,7 @@ #include #include "Application.h" +#include "settings/SettingsObject.h" #include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -50,8 +51,9 @@ JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) {} -Task::Ptr JavaInstallList::getLoadTask() +Task::Ptr JavaInstallList::getLoadTask(bool forceReload) { + Q_UNUSED(forceReload) load(); return getCurrentTask(); } @@ -107,7 +109,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const case VersionRole: return version->id.toString(); case RecommendedRole: - return version->recommended; + return false; case PathRole: return version->path; case CPUArchitectureRole: @@ -127,10 +129,6 @@ void JavaInstallList::updateListData(QList versions) beginResetModel(); m_vlist = versions; sortVersions(); - if (m_vlist.size()) { - auto best = std::dynamic_pointer_cast(m_vlist[0]); - best->recommended = true; - } endResetModel(); m_status = Status::Done; m_load_task.reset(); diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index c68c2a3be..58d1ad8ca 100644 --- a/launcher/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList { public: explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); - Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask(bool forceReload = false) override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp index 115baa9e5..3647c963f 100644 --- a/launcher/java/JavaMetadata.cpp +++ b/launcher/java/JavaMetadata.cpp @@ -111,7 +111,7 @@ bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } @@ -120,7 +120,7 @@ bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h index 2e569ee39..0757a6935 100644 --- a/launcher/java/JavaMetadata.h +++ b/launcher/java/JavaMetadata.h @@ -61,4 +61,4 @@ DownloadType parseDownloadType(QString javaDownload); QString downloadTypeToString(DownloadType javaDownload); MetadataPtr parseJavaMeta(const QJsonObject& libObj); -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 4f7a0ae3f..c58fe5601 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -42,6 +42,7 @@ #include #include "Application.h" +#include "BuildConfig.h" #include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -155,7 +156,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() QStringList addJavasFromEnv(QList javas) { - auto env = qEnvironmentVariable("PRISMLAUNCHER_JAVA_PATHS"); // FIXME: use launcher name from buildconfig + auto env = QProcessEnvironment::systemEnvironment().value(QStringLiteral("%1_JAVA_PATHS").arg(BuildConfig.LAUNCHER_ENVNAME)); #if defined(Q_OS_WIN32) QList javaPaths = env.replace("\\", "/").split(QLatin1String(";")); @@ -483,7 +484,8 @@ QList JavaUtils::FindJavaPaths() QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); // javas downloaded by gradle (toolchains) - scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); + QString gradleUserHome = qEnvironmentVariable("GRADLE_USER_HOME", FS::PathCombine(home, ".gradle")); + scanJavaDirs(FS::PathCombine(gradleUserHome, "jdks")); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index 92357930b..c60908cec 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -114,4 +114,4 @@ bool ArchiveDownloadTask::abort() aborted = m_task->abort(); return aborted; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h index 4cd919543..cfcdf9dcf 100644 --- a/launcher/java/download/ArchiveDownloadTask.h +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -42,4 +42,4 @@ class ArchiveDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 04d28a5cc..0a51741a2 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -39,9 +39,8 @@ void ManifestDownloadTask::executeTask() { setStatus(tr("Downloading Java")); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); - auto files = std::make_shared(); - auto action = Net::Download::makeByteArray(m_url, files); + auto [action, files] = Net::Download::makeByteArray(m_url); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { @@ -61,7 +60,7 @@ void ManifestDownloadTask::executeTask() QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response at " << parse_error.offset << ". Reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *files; emitFailed(parse_error.errorString()); return; diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h index 0f65b343c..e68c8236f 100644 --- a/launcher/java/download/ManifestDownloadTask.h +++ b/launcher/java/download/ManifestDownloadTask.h @@ -43,4 +43,4 @@ class ManifestDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/SymlinkTask.cpp b/launcher/java/download/SymlinkTask.cpp index 843c7caa9..9bbd50c63 100644 --- a/launcher/java/download/SymlinkTask.cpp +++ b/launcher/java/download/SymlinkTask.cpp @@ -78,4 +78,4 @@ void SymlinkTask::executeTask() } } -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/SymlinkTask.h b/launcher/java/download/SymlinkTask.h index 88cb20dd7..e38323eae 100644 --- a/launcher/java/download/SymlinkTask.h +++ b/launcher/java/download/SymlinkTask.h @@ -33,4 +33,4 @@ class SymlinkTask : public Task { QString m_path; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 9985e0f4f..26b2b582d 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -51,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) +std::unique_ptr LaunchTask::create(MinecraftInstance* inst) { - shared_qobject_ptr proc(new LaunchTask(inst)); - proc->init(); - return proc; + auto task = std::unique_ptr(new LaunchTask(inst)); + task->init(); + return task; } -LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstance* instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { @@ -76,6 +76,7 @@ void LaunchTask::executeTask() if (!m_steps.size()) { state = LaunchTask::Finished; emitSucceeded(); + return; } state = LaunchTask::Running; onStepFinished(); @@ -179,7 +180,7 @@ bool LaunchTask::abort() return true; case LaunchTask::NotStarted: { state = LaunchTask::Aborted; - emitFailed("Aborted"); + emitAborted(); return true; } case LaunchTask::Running: diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index db7e453e4..c52273a9e 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -39,7 +39,6 @@ #include #include #include -#include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" @@ -48,21 +47,21 @@ class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(MinecraftInstancePtr instance); + explicit LaunchTask(MinecraftInstance* instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static shared_qobject_ptr create(MinecraftInstancePtr inst); + static std::unique_ptr create(MinecraftInstance* inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - MinecraftInstancePtr instance() { return m_instance; } + MinecraftInstance* instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -119,7 +118,7 @@ class LaunchTask : public Task { bool parseXmlLogs(QString const& line, MessageLevel level); protected: /* data */ - MinecraftInstancePtr m_instance; + MinecraftInstance* m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; diff --git a/launcher/launch/TaskStepWrapper.cpp b/launcher/launch/TaskStepWrapper.cpp index db9e8fad2..acf790a8f 100644 --- a/launcher/launch/TaskStepWrapper.cpp +++ b/launcher/launch/TaskStepWrapper.cpp @@ -23,10 +23,7 @@ void TaskStepWrapper::executeTask() return; } connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); - connect(m_task.get(), &Task::progress, this, &TaskStepWrapper::setProgress); - connect(m_task.get(), &Task::stepProgress, this, &TaskStepWrapper::propagateStepProgress); - connect(m_task.get(), &Task::status, this, &TaskStepWrapper::setStatus); - connect(m_task.get(), &Task::details, this, &TaskStepWrapper::setDetails); + propagateFromOther(m_task.get()); emit progressReportingRequest(); } @@ -59,9 +56,7 @@ bool TaskStepWrapper::canAbort() const bool TaskStepWrapper::abort() { if (m_task && m_task->canAbort()) { - auto status = m_task->abort(); - emitFailed("Aborted."); - return status; + return m_task->abort(); } return Task::abort(); } diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 0f8d27e94..1afd5cd1d 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -36,7 +36,6 @@ #include "CheckJava.h" #include #include -#include #include #include #include @@ -68,7 +67,7 @@ void CheckJava::executeTask() emitFailed(QString("Java path is not valid.")); return; } else { - emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); + emit logLine("Java path is:\n " + m_javaPath, MessageLevel::Launcher); } if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -147,6 +146,6 @@ void CheckJava::checkJavaFinished(const JavaChecker::Result& result) void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor) { emit logLine( - QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n").arg(version, architecture, realArchitecture, vendor), + QString("Java is version %1, using %2 (%3) architecture, from %4").arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp index 4b67b3092..cb2f5d7de 100644 --- a/launcher/launch/steps/LookupServerAddress.cpp +++ b/launcher/launch/steps/LookupServerAddress.cpp @@ -38,7 +38,7 @@ void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) bool LookupServerAddress::abort() { m_dnsLookup->abort(); - emitFailed("Aborted"); + emitAborted(); return true; } @@ -87,6 +87,6 @@ void LookupServerAddress::resolve(const QString& address, quint16 port) m_output->address = address; m_output->port = port; - emitSucceeded(); m_dnsLookup->deleteLater(); + emitSucceeded(); } diff --git a/launcher/launch/steps/PrintServers.cpp b/launcher/launch/steps/PrintServers.cpp index ba96d37b9..ac0e4bf83 100644 --- a/launcher/launch/steps/PrintServers.cpp +++ b/launcher/launch/steps/PrintServers.cpp @@ -34,7 +34,7 @@ void PrintServers::executeTask() void PrintServers::resolveServer(const QHostInfo& host_info) { QString server = host_info.hostName(); - QString addresses = server + " resolves to:\n ["; + QString addresses = server + " resolves to:\n "; if (!host_info.addresses().isEmpty()) { for (QHostAddress address : host_info.addresses()) { @@ -46,7 +46,7 @@ void PrintServers::resolveServer(const QHostInfo& host_info) } else { addresses += "N/A"; } - addresses += "]\n\n"; + addresses += "\n"; m_server_to_address.insert(server, addresses); diff --git a/launcher/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp index 53aff807e..f96d11343 100644 --- a/launcher/launch/steps/TextPrint.cpp +++ b/launcher/launch/steps/TextPrint.cpp @@ -24,6 +24,6 @@ bool TextPrint::canAbort() const bool TextPrint::abort() { - emitFailed("Aborted."); + emitAborted(); return true; } diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp index e5021a616..b808b35a3 100644 --- a/launcher/logs/AnonymizeLog.cpp +++ b/launcher/logs/AnonymizeLog.cpp @@ -65,4 +65,4 @@ void anonymizeLog(QString& log) for (auto rule : anonymizeRules) { log.replace(rule.reg, rule.with); } -} \ No newline at end of file +} diff --git a/launcher/logs/AnonymizeLog.h b/launcher/logs/AnonymizeLog.h index 2409ecee7..215d1e468 100644 --- a/launcher/logs/AnonymizeLog.h +++ b/launcher/logs/AnonymizeLog.h @@ -37,4 +37,4 @@ #include -void anonymizeLog(QString& log); \ No newline at end of file +void anonymizeLog(QString& log); diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.h b/launcher/macsandbox/SecurityBookmarkFileAccess.h index 5bddf0e31..69b344af9 100644 --- a/launcher/macsandbox/SecurityBookmarkFileAccess.h +++ b/launcher/macsandbox/SecurityBookmarkFileAccess.h @@ -42,7 +42,8 @@ class SecurityBookmarkFileAccess { bool m_readOnly; NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale); -public: + + public: /// \param readOnly A boolean indicating whether the bookmark should be read-only. SecurityBookmarkFileAccess(bool readOnly = false); ~SecurityBookmarkFileAccess(); @@ -86,4 +87,4 @@ public: bool isAccessingPath(const QString& path); }; -#endif //FILEACCESS_H +#endif // FILEACCESS_H diff --git a/launcher/main.cpp b/launcher/main.cpp index 2bce655d2..9378387bb 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -33,13 +33,23 @@ * limitations under the License. */ +#include + #include "Application.h" +#if defined Q_OS_WIN32 +#include "console/WindowsConsole.h" +#endif + int main(int argc, char* argv[]) { +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + console::WindowsConsoleGuard _consoleGuard; +#endif + // initialize Qt Application app(argc, argv); - switch (app.status()) { case Application::StartingUp: case Application::Initialized: { diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index b0e754ada..1869e14d3 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -26,6 +26,7 @@ #include "net/NetJob.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "BuildConfig.h" #include "tasks/Task.h" @@ -81,12 +82,12 @@ QUrl BaseEntity::url() const return QUrl(metaOverride).resolved(localFilename()); } -Task::Ptr BaseEntity::loadTask(Net::Mode mode) +Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload) { if (m_task && m_task->isRunning()) { return m_task; } - m_task.reset(new BaseEntityLoadTask(this, mode)); + m_task.reset(new BaseEntityLoadTask(this, mode, forceReload)); return m_task; } @@ -106,7 +107,9 @@ BaseEntity::LoadStatus BaseEntity::status() const return m_load_status; } -BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} +BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload) + : m_entity(parent), m_mode(mode), m_force_reload(forceReload) +{} void BaseEntityLoadTask::executeTask() { @@ -124,9 +127,11 @@ void BaseEntityLoadTask::executeTask() } // on online the hash needs to match - hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; + const auto& expected = m_entity->m_sha256; + const auto& actual = m_entity->m_file_sha256; + hashMatches = expected == actual; if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { - throw Exception("mismatched checksum"); + throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual)); } // load local file @@ -148,13 +153,18 @@ void BaseEntityLoadTask::executeTask() auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; - if (wasLoadedOffline || wasLoadedRemote) { + if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) { emitSucceeded(); return; } m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); auto url = m_entity->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); + if (m_force_reload) { + // clear validators so manual refreshes fetch a fresh body + entry->setETag({}); + entry->setRemoteChangedTimestamp({}); + } entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h index 17aa0cb87..32d8bdbb8 100644 --- a/launcher/meta/BaseEntity.h +++ b/launcher/meta/BaseEntity.h @@ -43,7 +43,7 @@ class BaseEntity { void setSha256(QString sha256); virtual void parse(const QJsonObject& obj) = 0; - [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); + [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online, bool forceReload = false); protected: QString m_sha256; // the expected sha256 @@ -58,7 +58,7 @@ class BaseEntityLoadTask : public Task { Q_OBJECT public: - explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); + explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload); ~BaseEntityLoadTask() override = default; virtual void executeTask() override; @@ -68,6 +68,7 @@ class BaseEntityLoadTask : public Task { private: BaseEntity* m_entity; Net::Mode m_mode; + bool m_force_reload = false; NetJob::Ptr m_task; }; } // namespace Meta diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index d0c7075cd..bd58215c4 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -15,6 +15,7 @@ #include "Index.h" +#include "Application.h" #include "JsonFormat.h" #include "QObjectPtr.h" #include "VersionList.h" @@ -135,7 +136,7 @@ void Index::connectVersionList(const int row, const VersionList::Ptr& list) Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) { - if (mode == Net::Mode::Offline) { + if (mode == Net::Mode::Offline || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) { return get(uid, version)->loadTask(mode); } diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index dfca52d87..fa3a271a6 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -32,11 +32,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( setObjectName("Version list: " + uid); } -Task::Ptr VersionList::getLoadTask() +Task::Ptr VersionList::getLoadTask(bool forceReload) { 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)); + loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload)); + loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload)); return loadTask; } diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 18681b8ed..709c361de 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -37,7 +37,7 @@ class VersionList : public BaseVersionList, public BaseEntity { enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; bool isLoaded() override; - Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask(bool forceReload = false) override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; diff --git a/launcher/minecraft/Agent.h b/launcher/minecraft/Agent.h index bc385a74e..2432679da 100644 --- a/launcher/minecraft/Agent.h +++ b/launcher/minecraft/Agent.h @@ -4,26 +4,10 @@ #include "Library.h" -class Agent; - -using AgentPtr = std::shared_ptr; - -class Agent { - public: - Agent(LibraryPtr library, const QString& argument) - { - m_library = library; - m_argument = argument; - } - - public: /* methods */ - LibraryPtr library() { return m_library; } - QString argument() { return m_argument; } - - protected: /* data */ +struct Agent { /// The library pointing to the jar this Java agent is contained within - LibraryPtr m_library; + LibraryPtr library; /// The argument to the Java agent, passed after an = if present - QString m_argument; + QString argument; }; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 410d1e689..37d02b5c1 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -52,6 +52,7 @@ #include "Application.h" #include "net/NetRequest.h" +#include "update/AssetUpdateTask.h" namespace { QSet collectPathsFromDir(QString dirPath) @@ -103,7 +104,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to read assets index file" << path; + qCritical() << "Failed to read assets index file" << path << "error:" << file.errorString(); return false; } index.id = assetsId; @@ -298,7 +299,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + auto resourceURL = AssetUpdateTask::resourceUrl(); return resourceURL + getRelPath(); } diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 5f114e942..6a8bb27c0 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -479,6 +479,7 @@ void Component::clearUpdateAction() QDebug operator<<(QDebug d, const Component& comp) { - d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + QDebugStateSaver saver(d); + d.nospace() << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; return d; } diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 56db20205..73203f74b 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -38,6 +38,9 @@ * If the component list changes, start over. */ +/* + * TODO: This task launches multiple other tasks. As such it should be converted to a ConcurrentTask + */ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); @@ -48,9 +51,38 @@ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfi ComponentUpdateTask::~ComponentUpdateTask() {} +bool ComponentUpdateTask::canAbort() const +{ + for (const auto& status : d->remoteLoadStatusList) { + if (status.task && !status.task->canAbort()) { + return false; + } + } + + return true; +} + +bool ComponentUpdateTask::abort() +{ + bool aborted = true; + for (const auto& status : d->remoteLoadStatusList) { + if (status.task && !status.task->abort()) { + aborted = false; + } + } + + return aborted; +} + +Net::Mode ComponentUpdateTask::netMode() +{ + return d->netmode; +} + void ComponentUpdateTask::executeTask() { qCDebug(instanceProfileResolveC) << "Loading components"; + setStatus(tr("Loading components")); loadComponents(); } @@ -196,12 +228,13 @@ void ComponentUpdateTask::loadComponents() componentIndex++; } d->remoteTasksInProgress = taskIndex; + m_progressTotal = static_cast(taskIndex); switch (result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. performUpdateActions(); resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); - break; + return; } case LoadResult::RequiresRemote: { // we wait for signals. @@ -209,9 +242,11 @@ void ComponentUpdateTask::loadComponents() } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); - break; + return; } } + + setDetails(tr("Downloading metadata for %1 components").arg(taskIndex)); } namespace { @@ -521,7 +556,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) // change a version of something that exists for (auto& change : toChange) { // FIXME: this should not work directly with the component list - qCDebug(instanceProfileResolveC) << "Setting version of " << change.uid << "to" << change.equalsVersion; + qCDebug(instanceProfileResolveC) << "Setting version of" << change.uid << "to" << change.equalsVersion; auto component = componentIndex[change.uid]; component->setVersion(change.equalsVersion); } @@ -744,7 +779,7 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } - qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed: " << msg; + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed:" << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; @@ -754,6 +789,7 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) void ComponentUpdateTask::checkIfAllFinished() { + setProgress(m_progress + 1, m_progressTotal); if (d->remoteTasksInProgress) { // not yet... return; @@ -773,8 +809,9 @@ void ComponentUpdateTask::checkIfAllFinished() .arg(component->getName(), component->m_version)); } } + d->remoteLoadStatusList.clear(); + auto allErrors = allErrorsList.join("\n"); emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); - d->remoteLoadStatusList.clear(); } } diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 64c55877b..2ef9737ba 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -17,8 +17,12 @@ class ComponentUpdateTask : public Task { explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); + bool canAbort() const override; + bool abort() override; + Net::Mode netMode(); + protected: - void executeTask(); + void executeTask() override; private: void loadComponents(); diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index a2588064f..65297abed 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -38,12 +38,10 @@ #include #include #include -#include "DefaultVariable.h" struct GradleSpecifier { GradleSpecifier() { m_valid = false; } - GradleSpecifier(QString value) { operator=(value); } - GradleSpecifier& operator=(const QString& value) + GradleSpecifier(const QString& value) { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar @@ -62,7 +60,7 @@ struct GradleSpecifier { m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; - return *this; + return; } auto elements = match.captured(); m_groupId = match.captured(1); @@ -72,7 +70,6 @@ struct GradleSpecifier { if (match.lastCapturedIndex() >= 5) { m_extension = match.captured(5); } - return *this; } QString serialize() const { @@ -83,8 +80,8 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { retval += ":" + m_classifier; } - if (m_extension.isExplicit()) { - retval += "@" + m_extension; + if (m_extension.has_value()) { + retval += "@" + m_extension.value(); } return retval; } @@ -97,7 +94,7 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { filename += "-" + m_classifier; } - filename += "." + m_extension; + filename += "." + m_extension.value_or("jar"); return filename; } QString toPath(const QString& filenameOverride = QString()) const @@ -122,26 +119,13 @@ struct GradleSpecifier { inline QString artifactId() const { return m_artifactId; } inline void setClassifier(const QString& classifier) { m_classifier = classifier; } inline QString classifier() const { return m_classifier; } - inline QString extension() const { return m_extension; } + inline std::optional extension() const { return m_extension; } inline QString artifactPrefix() const { return m_groupId + ":" + m_artifactId; } bool matchName(const GradleSpecifier& other) const { return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } - bool operator==(const GradleSpecifier& other) const - { - if (m_groupId != other.m_groupId) - return false; - if (m_artifactId != other.m_artifactId) - return false; - if (m_version != other.m_version) - return false; - if (m_classifier != other.m_classifier) - return false; - if (m_extension != other.m_extension) - return false; - return true; - } + bool operator ==(const GradleSpecifier &other) const = default; private: QString m_invalidValue; @@ -149,6 +133,6 @@ struct GradleSpecifier { QString m_artifactId; QString m_version; QString m_classifier; - DefaultVariable m_extension = DefaultVariable("jar"); + std::optional m_extension; bool m_valid = false; }; diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index c11a0f915..fb74d4a9a 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -213,9 +213,9 @@ void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& r m_mavenFiles.append(Library::limitedCopy(mavenFile)); } -void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext) +void LaunchProfile::applyAgent(const Agent& agent, const RuntimeContext& runtimeContext) { - auto lib = agent->library(); + auto lib = agent.library; if (!lib->isActive(runtimeContext)) { return; } @@ -330,7 +330,7 @@ const QList& LaunchProfile::getMavenFiles() const return m_mavenFiles; } -const QList& LaunchProfile::getAgents() const +const QList& LaunchProfile::getAgents() const { return m_agents; } @@ -349,7 +349,8 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath) const + const QString& tempPath, + bool addJarMods) const { QStringList native32, native64; jars.clear(); @@ -360,7 +361,7 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, // NOTE: order is important here, add main jar last to the lists if (m_mainJar) { // FIXME: HACK!! jar modding is weird and unsystematic! - if (m_jarMods.size()) { + if (m_jarMods.size() && addJarMods) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index f1be6fee0..6dc3d9aeb 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -57,7 +57,7 @@ class LaunchProfile : public ProblemProvider { void applyMods(const QList& jarMods); void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); - void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); + void applyAgent(const Agent& agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); @@ -79,7 +79,7 @@ class LaunchProfile : public ProblemProvider { const QList& getLibraries() const; const QList& getNativeLibraries() const; const QList& getMavenFiles() const; - const QList& getAgents() const; + const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; @@ -87,7 +87,8 @@ class LaunchProfile : public ProblemProvider { QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath) const; + const QString& tempPath, + bool addJarMods = true) const; bool hasTrait(const QString& trait) const; ProblemSeverity getProblemSeverity() const override; const QList getProblems() const override; @@ -132,7 +133,7 @@ class LaunchProfile : public ProblemProvider { QList m_mavenFiles; /// the list of java agents to add to JVM arguments - QList m_agents; + QList m_agents; /// the main jar LibraryPtr m_mainJar; diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 026f9c281..2b43d4389 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -149,7 +149,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC if (sha1.size()) { auto dl = Net::ApiDownload::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); - qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url << "expected sha1:" << sha1; out.append(dl); } else { out.append(Net::ApiDownload::makeCached(url, entry, options)); diff --git a/launcher/minecraft/Logging.cpp b/launcher/minecraft/Logging.cpp index 92596de3e..8b6304205 100644 --- a/launcher/minecraft/Logging.cpp +++ b/launcher/minecraft/Logging.cpp @@ -19,7 +19,6 @@ */ #include "minecraft/Logging.h" -#include Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index cdf670b0f..8e98a2efe 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -40,13 +40,6 @@ #include "BuildConfig.h" #include "Json.h" #include "QObjectPtr.h" -#include "minecraft/launch/AutoInstallJava.h" -#include "minecraft/launch/CreateGameFolders.h" -#include "minecraft/launch/ExtractNatives.h" -#include "minecraft/launch/PrintInstanceInfo.h" -#include "minecraft/update/AssetUpdateTask.h" -#include "minecraft/update/FMLLibrariesTask.h" -#include "minecraft/update/LibrariesTask.h" #include "settings/Setting.h" #include "settings/SettingsObject.h" @@ -63,13 +56,24 @@ #include "launch/steps/QuitAfterGameStop.h" #include "launch/steps/TextPrint.h" +#include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/ClaimAccount.h" +#include "minecraft/launch/CreateGameFolders.h" +#include "minecraft/launch/EnsureAvailableMemory.h" +#include "minecraft/launch/EnsureOfflineLibraries.h" +#include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" +#include "minecraft/launch/PrintInstanceInfo.h" #include "minecraft/launch/ReconstructAssets.h" #include "minecraft/launch/ScanModFolders.h" #include "minecraft/launch/VerifyJavaInstall.h" +#include "minecraft/update/AssetUpdateTask.h" +#include "minecraft/update/FoldersTask.h" +#include "minecraft/update/LegacyFMLLibrariesTask.h" +#include "minecraft/update/LibrariesTask.h" + #include "java/JavaUtils.h" #include "icons/IconList.h" @@ -84,7 +88,6 @@ #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" -#include "minecraft/update/FoldersTask.h" #include "tools/BaseProfiler.h" @@ -95,7 +98,7 @@ #include #ifdef Q_OS_LINUX -#include "MangoHud.h" +#include "LibraryUtils.h" #endif #ifdef WITH_QTDBUS @@ -128,7 +131,8 @@ for (const auto& gpu : gpus) { QString name = qvariant_cast(gpu[QStringLiteral("Name")]); bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); - if (!defaultGpu) { + bool discrete = qvariant_cast(gpu.value(QStringLiteral("Discrete"), !defaultGpu)); + if (discrete) { QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); for (int i = 0; i + 1 < envList.size(); i += 2) { env.insert(envList[i], envList[i + 1]); @@ -160,12 +164,14 @@ class OrSetting : public Setting { std::shared_ptr m_b; }; -MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) - : BaseInstance(globalSettings, settings, rootDir) +MinecraftInstance::MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) + : BaseInstance(globalSettings, std::move(settings), rootDir) { m_components.reset(new PackProfile(this)); } +MinecraftInstance::~MinecraftInstance() {} + void MinecraftInstance::saveNow() { m_components->saveNow(); @@ -204,6 +210,7 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting); // Native library workarounds auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); @@ -269,7 +276,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { - m_runtimeContext.updateFromInstanceSettings(m_settings); + m_runtimeContext.updateFromInstanceSettings(m_settings.get()); m_components->invalidateLaunchProfile(); } @@ -278,9 +285,9 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -std::shared_ptr MinecraftInstance::getPackProfile() const +PackProfile* MinecraftInstance::getPackProfile() const { - return m_components; + return m_components.get(); } QSet MinecraftInstance::traits() const @@ -308,9 +315,9 @@ void MinecraftInstance::populateLaunchMenu(QMenu* menu) normalLaunchDemo->setEnabled(supportsDemo()); - connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this()); }); - connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, false); }); - connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, true); }); + connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(this); }); + connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Offline); }); + connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Demo); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); @@ -484,7 +491,7 @@ QStringList MinecraftInstance::getNativeJars() return nativeJars; } -static QString replaceTokensIn(const QString &text, const QMap &with) +static QString replaceTokensIn(const QString& text, const QMap& with) { // TODO: does this still work?? QString result; @@ -506,7 +513,6 @@ static QString replaceTokensIn(const QString &text, const QMap return result; } - QStringList MinecraftInstance::extraArguments() { auto list = BaseInstance::extraArguments(); @@ -521,15 +527,15 @@ QStringList MinecraftInstance::extraArguments() if (!addn.isEmpty()) { QMap tokenMapping = makeProfileVarMapping(m_components->getProfile()); - for (const QString &item : addn) { + for (const QString& item : addn) { list.append(replaceTokensIn(item, tokenMapping)); } } auto agents = m_components->getProfile()->getAgents(); - for (auto agent : agents) { + for (const auto& agent : agents) { QStringList jar, temp1, temp2, temp3; - agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); - list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); + agent.library->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); + list.append("-javaagent:" + jar[0] + (agent.argument.isEmpty() ? "" : "=" + agent.argument)); } { @@ -565,6 +571,8 @@ QStringList MinecraftInstance::javaArguments() { QStringList args; + args << "-Duser.language=en"; + // custom args go first. we want to override them if we have our own here. args.append(extraArguments()); @@ -618,8 +626,6 @@ QStringList MinecraftInstance::javaArguments() } } - args << "-Duser.language=en"; - if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; @@ -648,7 +654,7 @@ QMap MinecraftInstance::getVariables() out.insert("INST_ID", id()); out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); - out.insert("INST_JAVA", settings()->get("JavaPath").toString()); + out.insert("INST_JAVA", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); #ifdef Q_OS_MACOS @@ -682,12 +688,7 @@ QProcessEnvironment MinecraftInstance::createEnvironment() env.insert(iter.key(), iter.value().toString()); }; - bool overrideEnv = settings()->get("OverrideEnv").toBool(); - - if (!overrideEnv) - insertEnv(APPLICATION->settings()->get("Env").toString()); - else - insertEnv(settings()->get("Env").toString()); + insertEnv(settings()->get("Env").toString()); return env; } @@ -702,7 +703,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); - auto mangoHudLibString = MangoHud::getLibraryString(); + auto mangoHudLibString = LibraryUtils::findMangoHud(); if (!mangoHudLibString.isEmpty()) { QFileInfo mangoHudLib(mangoHudLibString); QString libPath = mangoHudLib.absolutePath(); @@ -738,6 +739,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); env.insert("GALLIUM_DRIVER", "zink"); + env.insert("LIBGL_KOPPER_DRI2", "1"); } #endif return env; @@ -746,25 +748,24 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); - QString args_pattern = profile->getMinecraftArguments(); + auto args = profile->getMinecraftArguments().split(' ', Qt::SkipEmptyParts); for (auto tweaker : profile->getTweakers()) { - args_pattern += " --tweakClass " + tweaker; + args << "--tweakClass" << tweaker; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { if (profile->hasTrait("feature:is_quick_play_multiplayer")) { - args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + args << "--quickPlayMultiplayer" << targetToJoin->address + ':' + QString::number(targetToJoin->port); } else { - args_pattern += " --server " + targetToJoin->address; - args_pattern += " --port " + QString::number(targetToJoin->port); + args << "--server" << targetToJoin->address; + args << "--port" << QString::number(targetToJoin->port); } } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { - args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; + args << "--quickPlaySingleplayer" << targetToJoin->world; } } - QMap tokenMapping = makeProfileVarMapping(profile); // yggdrasil! @@ -777,16 +778,15 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine tokenMapping["user_properties"] = session->serializeUserProperties(); tokenMapping["user_type"] = session->user_type; - if (session->demo) { - args_pattern += " --demo"; + if (session->launchMode == LaunchMode::Demo) { + args << "--demo"; } } - QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); - for (int i = 0; i < parts.length(); i++) { - parts[i] = replaceTokensIn(parts[i], tokenMapping); + for (int i = 0; i < args.length(); i++) { + args[i] = replaceTokensIn(args[i], tokenMapping); } - return parts; + return args; } QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) @@ -888,58 +888,24 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { + constexpr auto indent = " "; + constexpr auto emptyLine = ""; + QStringList out; - out << "Main Class:" << " " + getMainClass() << ""; - out << "Native path:" << " " + getNativePath() << ""; + + out << "Components:"; + for (int i = 0; i < m_components->rowCount(); ++i) { + const auto& component = m_components->getComponent(i); + out << indent + + QString("%1) %2 (%3) %4").arg(QString::number(i + 1), component->getName(), component->getID(), component->getVersion()); + } + out << emptyLine; + + out << "Launcher: " + getLauncher(); + out << "Main class: " + getMainClass() << emptyLine; auto profile = m_components->getProfile(); - // traits - auto alltraits = traits(); - if (alltraits.size()) { - out << "Traits:"; - for (auto trait : alltraits) { - out << "traits " + trait; - } - out << ""; - } - - // native libraries - auto settings = this->settings(); - bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); - bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); - if (nativeOpenAL || nativeGLFW) { - if (nativeOpenAL) - out << "Using system OpenAL."; - if (nativeGLFW) - out << "Using system GLFW."; - out << ""; - } - - // libraries and class path. - { - out << "Libraries:"; - QStringList jars, nativeJars; - profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); - auto printLibFile = [&out](const QString& path) { - QFileInfo info(path); - if (info.exists()) { - out << " " + path; - } else { - out << " " + path + " (missing)"; - } - }; - for (auto file : jars) { - printLibFile(file); - } - out << ""; - out << "Native libraries:"; - for (auto file : nativeJars) { - printLibFile(file); - } - out << ""; - } - // mods and core mods auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { @@ -962,12 +928,12 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } - out << ""; + out << emptyLine; } }; - printModList("Mods", *(loaderModList().get())); - printModList("Core Mods", *(coreModList().get())); + printModList("Mods", *loaderModList()); + printModList("Core Mods", *coreModList()); // jar mods auto& jarMods = profile->getJarMods(); @@ -977,19 +943,59 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto displayname = jarmod->displayName(runtimeContext()); auto realname = jarmod->filename(runtimeContext()); if (displayname != realname) { - out << " " + displayname + " (" + realname + ")"; + out << indent + displayname + " (" + realname + ")"; } else { - out << " " + realname; + out << indent + realname; } } - out << ""; + out << emptyLine; } + // traits + auto alltraits = traits(); + if (alltraits.size()) { + out << "Traits:"; + for (auto trait : alltraits) { + out << indent + trait; + } + out << emptyLine; + } + + // native libraries + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << emptyLine; + } + + // libraries and class path. + { + out << "Libraries:"; + QStringList jars, nativeJars; + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + for (auto file : jars) { + out << indent + file; + } + out << emptyLine; + out << "Native libraries:"; + for (auto file : nativeJars) { + out << indent + file; + } + out << emptyLine; + } + + out << "Natives path:" << indent + getNativePath() << emptyLine; + // minecraft arguments auto params = processMinecraftArgs(nullptr, targetToJoin); - out << "Params:"; - out << " " + params.join(' '); - out << ""; + out << "Minecraft arguments:"; + out << indent + params.join(' '); + out << emptyLine; // window size QString windowParams; @@ -1000,9 +1006,18 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } - out << ""; - out << "Launcher: " + getLauncher(); - out << ""; + out << emptyLine; + + // environment variables + const QString env = settings->get("Env").toString(); + if (auto envMap = Json::toMap(env); !envMap.isEmpty()) { + out << "Custom environment variables:"; + for (auto [key, value] : envMap.asKeyValueRange()) { + out << indent + key + "=" + value.toString(); + } + out << emptyLine; + } + return out; } @@ -1102,24 +1117,23 @@ QList MinecraftInstance::createUpdateTask() // libraries download makeShared(this), // FML libraries download and copy into the instance - makeShared(this), + makeShared(this), // assets update makeShared(this), }; } -shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); - // FIXME: get rid of shared_from_this ... - auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_this())); + auto process = LaunchTask::create(this); auto pptr = process.get(); APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { - process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); + process->appendStep(makeShared(pptr, "Minecraft folder is:\n " + gameRoot() + "\n", MessageLevel::Launcher)); } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) @@ -1149,7 +1163,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // load meta { - auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; process->appendStep(makeShared(pptr, makeShared(this, mode))); } @@ -1157,6 +1171,8 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->appendStep(makeShared(pptr)); process->appendStep(makeShared(pptr)); + // verify that minimum Java requirements are met + process->appendStep(makeShared(pptr)); } // run pre-launch command if that's needed @@ -1166,14 +1182,14 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } - // if we aren't in offline mode,. - if (session->status != AuthSession::PlayableOffline) { - if (!session->demo) { - process->appendStep(makeShared(pptr, session)); - } + // if we aren't in offline mode + if (session->launchMode != LaunchMode::Offline) { + process->appendStep(makeShared(pptr, session)); for (auto t : createUpdateTask()) { process->appendStep(makeShared(pptr, t)); } + } else { + process->appendStep(makeShared(pptr, this)); } // if there are any jar mods @@ -1186,6 +1202,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } + // make sure we have enough RAM, warn the user if we don't + { + process->appendStep(makeShared(pptr, this)); + } + // print some instance info here... { process->appendStep(makeShared(pptr, session, targetToJoin)); @@ -1201,11 +1222,6 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } - // verify that minimum Java requirements are met - { - process->appendStep(makeShared(pptr)); - } - { // actually launch the game auto step = makeShared(pptr); @@ -1227,9 +1243,9 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if (m_settings->get("QuitAfterGameStop").toBool()) { process->appendStep(makeShared(pptr)); } - m_launchProcess = process; - emit launchTaskChanged(m_launchProcess); - return m_launchProcess; + m_launchProcess = std::move(process); + emit launchTaskChanged(m_launchProcess.get()); + return m_launchProcess.get(); } JavaVersion MinecraftInstance::getJavaVersion() @@ -1237,80 +1253,80 @@ JavaVersion MinecraftInstance::getJavaVersion() return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr MinecraftInstance::loaderModList() +ModFolderModel* 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, true)); } - return m_loader_mod_list; + return m_loader_mod_list.get(); } -std::shared_ptr MinecraftInstance::coreModList() +ModFolderModel* 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, true)); } - return m_core_mod_list; + return m_core_mod_list.get(); } -std::shared_ptr MinecraftInstance::nilModList() +ModFolderModel* MinecraftInstance::nilModList() { if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); } - return m_nil_mod_list; + return m_nil_mod_list.get(); } -std::shared_ptr MinecraftInstance::resourcePackList() +ResourcePackFolderModel* MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { 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; + return m_resource_pack_list.get(); } -std::shared_ptr MinecraftInstance::texturePackList() +TexturePackFolderModel* MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { 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; + return m_texture_pack_list.get(); } -std::shared_ptr MinecraftInstance::shaderPackList() +ShaderPackFolderModel* MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { 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; + return m_shader_pack_list.get(); } -std::shared_ptr MinecraftInstance::dataPackList() +DataPackFolderModel* MinecraftInstance::dataPackList() { if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); } - return m_data_pack_list; + return m_data_pack_list.get(); } -QList> MinecraftInstance::resourceLists() +QList MinecraftInstance::resourceLists() { return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } -std::shared_ptr MinecraftInstance::worldList() +WorldList* MinecraftInstance::worldList() { if (!m_world_list) { m_world_list.reset(new WorldList(worldDir(), this)); } - return m_world_list; + return m_world_list.get(); } QList MinecraftInstance::getJarMods() const diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index ecddef69c..909962d5e 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -56,8 +56,8 @@ class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: - MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); - virtual ~MinecraftInstance() = default; + MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); + virtual ~MinecraftInstance(); virtual void saveNow() override; void loadSpecificSettings() override; @@ -109,22 +109,22 @@ class MinecraftInstance : public BaseInstance { void updateRuntimeContext() override; ////// Profile management ////// - std::shared_ptr getPackProfile() const; + PackProfile* getPackProfile() const; ////// Mod Lists ////// - std::shared_ptr loaderModList(); - std::shared_ptr coreModList(); - std::shared_ptr nilModList(); - std::shared_ptr resourcePackList(); - std::shared_ptr texturePackList(); - std::shared_ptr shaderPackList(); - std::shared_ptr dataPackList(); - QList> resourceLists(); - std::shared_ptr worldList(); + ModFolderModel* loaderModList(); + ModFolderModel* coreModList(); + ModFolderModel* nilModList(); + ResourcePackFolderModel* resourcePackList(); + TexturePackFolderModel* texturePackList(); + ShaderPackFolderModel* shaderPackList(); + DataPackFolderModel* dataPackList(); + QList resourceLists(); + WorldList* worldList(); ////// Launch stuff ////// QList createUpdateTask() override; - shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; + LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; @@ -162,15 +162,13 @@ class MinecraftInstance : public BaseInstance { QMap makeProfileVarMapping(std::shared_ptr profile) const; protected: // data - std::shared_ptr m_components; - mutable std::shared_ptr m_loader_mod_list; - mutable std::shared_ptr m_core_mod_list; - mutable std::shared_ptr m_nil_mod_list; - mutable std::shared_ptr m_resource_pack_list; - mutable std::shared_ptr m_shader_pack_list; - mutable std::shared_ptr m_texture_pack_list; - mutable std::shared_ptr m_data_pack_list; - mutable std::shared_ptr m_world_list; + std::unique_ptr m_components; + std::unique_ptr m_loader_mod_list; + std::unique_ptr m_core_mod_list; + std::unique_ptr m_nil_mod_list; + std::unique_ptr m_resource_pack_list; + std::unique_ptr m_shader_pack_list; + std::unique_ptr m_texture_pack_list; + std::unique_ptr m_data_pack_list; + std::unique_ptr m_world_list; }; - -using MinecraftInstancePtr = std::shared_ptr; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index c0a82e61e..c26fb8b60 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -20,11 +20,8 @@ void MinecraftLoadAndCheck::executeTask() } connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); - connect(m_task.get(), &Task::aborted, this, [this] { emitFailed(tr("Aborted")); }); - connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); - connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); - connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); - connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); + connect(m_task.get(), &Task::aborted, this, &MinecraftLoadAndCheck::emitAborted); + propagateFromOther(m_task.get()); } bool MinecraftLoadAndCheck::canAbort() const @@ -38,9 +35,7 @@ bool MinecraftLoadAndCheck::canAbort() const bool MinecraftLoadAndCheck::abort() { if (m_task && m_task->canAbort()) { - auto status = m_task->abort(); - emitFailed("Aborted."); - return status; + return m_task->abort(); } return Task::abort(); -} \ No newline at end of file +} diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 48ea3b894..95f4c5ef6 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -209,8 +209,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc QString arg = ""; readString(agentObj, "argument", arg); - AgentPtr agent(new Agent(lib, arg)); - out->agents.append(agent); + out->agents.append(Agent{ lib, arg }); } } @@ -305,10 +304,10 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch writeStringList(root, "+jvmArgs", patch->addnJvmArguments); if (!patch->agents.isEmpty()) { QJsonArray array; - for (auto value : patch->agents) { - QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); - if (!value->argument().isEmpty()) - agentOut.insert("argument", value->argument()); + for (const auto& value : patch->agents) { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value.library.get()); + if (!value.argument.isEmpty()) + agentOut.insert("argument", value.argument); array.append(agentOut); } @@ -370,8 +369,7 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemConta } // just make up something unique on the spot for the library name. - auto uuid = QUuid::createUuid(); - QString id = uuid.toString().remove('{').remove('}'); + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); // filename override is the old name diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 74c8d5ef8..f0cff7f0e 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -38,7 +38,6 @@ */ #include -#include #include #include #include @@ -168,6 +167,7 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c } if (!outFile.commit()) { qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); + return false; } return true; } @@ -230,9 +230,8 @@ static PackProfile::Result loadPackProfile(PackProfile* parent, void PackProfile::saveNow() { - if (saveIsScheduled()) { + if (saveIsScheduled() && save_internal()) { d->m_saveTimer.stop(); - save_internal(); } } @@ -280,12 +279,15 @@ QString PackProfile::patchFilePathForUid(const QString& uid) const return patchesPattern().arg(uid); } -void PackProfile::save_internal() +bool PackProfile::save_internal() { qDebug() << d->m_instance->name() << "|" << "Component list save performed now"; auto filename = componentsFilePath(); - savePackProfile(filename, d->components); - d->dirty = false; + if (savePackProfile(filename, d->components)) { + d->dirty = false; + return true; + } + return false; } PackProfile::Result PackProfile::load() @@ -324,7 +326,15 @@ 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 Result::Success(); + if (d->m_updateTask->netMode() == netmode) { + return Result::Success(); + } + + // https://github.com/PrismLauncher/PrismLauncher/issues/5209 + // FIXME: HACK HACK HACK + disconnect(d->m_updateTask.get(), &ComponentUpdateTask::aborted, nullptr, nullptr); + d->m_updateTask->abort(); + d->m_updateTask.reset(); } // flush any scheduled saves to not lose state @@ -364,7 +374,7 @@ void PackProfile::updateSucceeded() void PackProfile::updateFailed(const QString& error) { - qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task failed " << "Reason:" << error; + qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task failed. Reason:" << error; d->m_updateTask.reset(); invalidateLaunchProfile(); } @@ -915,7 +925,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths) agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); - versionFile->agents.append(std::make_shared(agent, QString())); + versionFile->agents.append(Agent{agent, QString()}); versionFile->name = targetName; versionFile->uid = targetId; @@ -952,7 +962,7 @@ std::shared_ptr PackProfile::getProfile() const } d->m_profile = profile; } catch (const Exception& error) { - qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Couldn't apply profile patches because: " << error.cause(); + qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Couldn't apply profile patches because:" << error.cause(); } } return d->m_profile; diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index d812dfa48..70dd04514 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -175,7 +175,7 @@ class PackProfile : public QAbstractListModel { QString patchesPattern() const; private slots: - void save_internal(); + bool save_internal(); void updateSucceeded(); void updateFailed(const QString& error); void componentDataChanged(); diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index 4fb3621f0..feb825904 100644 --- a/launcher/minecraft/PackProfile_p.h +++ b/launcher/minecraft/PackProfile_p.h @@ -22,7 +22,7 @@ struct PackProfileData { ComponentIndex componentIndex; bool dirty = false; QTimer m_saveTimer; - Task::Ptr m_updateTask; + shared_qobject_ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index a79f89529..ae6326953 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -55,7 +55,7 @@ bool readOverrideOrders(QString path, PatchOrder& order) return false; } if (!orderFile.open(QFile::ReadOnly)) { - qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); + qCritical() << "Couldn't open" << orderFile.fileName() << "for reading:" << orderFile.errorString(); qWarning() << "Ignoring overridden order"; return false; } @@ -144,13 +144,13 @@ bool saveJsonFile(const QJsonDocument& doc, const QString& filename) auto data = doc.toJson(); QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { + qWarning() << "Couldn't open" << filename << "for writing:" << jsonFile.errorString(); jsonFile.cancelWriting(); - qWarning() << "Couldn't open" << filename << "for writing"; return false; } jsonFile.write(data); if (!jsonFile.commit()) { - qWarning() << "Couldn't save" << filename; + qWarning() << "Couldn't save" << filename << "error:" << jsonFile.errorString(); return false; } return true; diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index ec4ebb31a..b719e3142 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -71,7 +71,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application: %1").arg(iconFile.errorString())); return false; } @@ -101,7 +101,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); @@ -127,7 +127,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index b995c36bd..5cf31f9b2 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -37,6 +37,7 @@ #pragma once #include "Application.h" +#include "BaseInstance.h" #include #include diff --git a/launcher/minecraft/VanillaInstanceCreationTask.cpp b/launcher/minecraft/VanillaInstanceCreationTask.cpp index ccbd8c677..e646e2c52 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.cpp +++ b/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -7,32 +7,27 @@ #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" -VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) - : InstanceCreationTask() - , m_version(std::move(version)) - , m_using_loader(true) - , m_loader(std::move(loader)) - , m_loader_version(std::move(loader_version)) +VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion) + : m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loaderVersion)) {} -bool VanillaCreationTask::createInstance() +std::unique_ptr VanillaCreationTask::createInstance() { setStatus(tr("Creating instance from version %1").arg(m_version->name())); - auto instance_settings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); - instance_settings->suspendSave(); - { - MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath); - auto components = inst.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", m_version->descriptor(), true); - if (m_using_loader) - components->setComponentVersion(m_loader, m_loader_version->descriptor()); + auto inst = std::make_unique( + m_globalSettings, std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), m_stagingPath); + SettingsObject::Lock lock(inst->settings()); - inst.setName(name()); - inst.setIconKey(m_instIcon); + auto* components = inst->getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if (m_using_loader) { + components->setComponentVersion(m_loader, m_loader_version->descriptor()); } - instance_settings->resumeSave(); - return true; + inst->setName(name()); + inst->setIconKey(m_instIcon); + components->saveNow(); + return inst; } diff --git a/launcher/minecraft/VanillaInstanceCreationTask.h b/launcher/minecraft/VanillaInstanceCreationTask.h index d1b816824..c1a69ab62 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.h +++ b/launcher/minecraft/VanillaInstanceCreationTask.h @@ -7,10 +7,10 @@ class VanillaCreationTask final : public InstanceCreationTask { Q_OBJECT public: - VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} - VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); + explicit VanillaCreationTask(BaseVersion::Ptr version) : m_version(std::move(version)) {} + VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion); - bool createInstance() override; + std::unique_ptr createInstance() override; private: // Version to update to / create of the instance. diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 40f49aaa4..32a7504ac 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -126,7 +126,7 @@ class VersionFile : public ProblemContainer { QList mavenFiles; /// Prism Launcher: list of agents to add to JVM arguments - QList agents; + QList agents; /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index bdbe721e3..0deecb042 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -153,18 +153,23 @@ QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) return val; } -QString getLevelDatFromFS(const QFileInfo& file) +QString getDatFromFS(const QFileInfo& root, QString file) { - QDir worldDir(file.filePath()); - if (!file.isDir() || !worldDir.exists("level.dat")) { + QDir worldDir(root.filePath()); + if (!root.isDir() || !worldDir.exists(file)) { return QString(); } - return worldDir.absoluteFilePath("level.dat"); + return worldDir.absoluteFilePath(file); } -QByteArray getLevelDatDataFromFS(const QFileInfo& file) +QString getLevelDatFromFS(const QFileInfo& file) { - auto fullFilePath = getLevelDatFromFS(file); + return getDatFromFS(file, "level.dat"); +} + +QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +{ + auto fullFilePath = getDatFromFS(root, file); if (fullFilePath.isNull()) { return QByteArray(); } @@ -175,6 +180,16 @@ QByteArray getLevelDatDataFromFS(const QFileInfo& file) return f.readAll(); } +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "level.dat"); +} + +QByteArray getWorldGenDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); +} + bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) { auto fullFilePath = getLevelDatFromFS(file); @@ -229,6 +244,8 @@ bool World::resetIcon() return false; } +int64_t loadSeed(QByteArray data); + void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); @@ -238,6 +255,12 @@ void World::readFromFS(const QFileInfo& file) } loadFromLevelDat(bytes); m_levelDatTime = file.lastModified(); + if (m_randomSeed == 0) { + bytes = getWorldGenDataFromFS(file); + if (!bytes.isEmpty()) { + m_randomSeed = loadSeed(bytes); + } + } } void World::readFromZip(const QFileInfo& file) @@ -251,9 +274,6 @@ void World::readFromZip(const QFileInfo& file) QFileInfo fi(filePath); if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { m_containerOffsetPath = filePath.chopped(levelDat.length()); - if (!m_containerOffsetPath.isEmpty()) { - return false; - } m_levelDatTime = file->dateTime(); loadFromLevelDat(file->readAll()); m_isValid = true; @@ -393,6 +413,28 @@ GameType read_gametype(nbt::value& parent, const char* name) } // namespace +int64_t loadSeed(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) { + return 0; + } + + nbt::value* valPtr = nullptr; + try { + valPtr = &levelData->at("data"); + } catch (const std::out_of_range&) { + return 0; + } + nbt::value& val = *valPtr; + + try { + return read_long(val, "seed").value_or(0); + } catch (const std::out_of_range&) { + } + return 0; +} + void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); @@ -405,7 +447,7 @@ void World::loadFromLevelDat(QByteArray data) try { valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { - qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + qWarning().nospace() << "Unable to read NBT tags from " << m_folderName << ": " << e.what(); m_isValid = false; return; } diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index cca931826..bb4baa33d 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -87,7 +87,7 @@ class World { QString m_iconFile; QDateTime m_levelDatTime; QDateTime m_lastPlayed; - int64_t m_size; + int64_t m_size = 0; int64_t m_randomSeed = 0; GameType m_gameType; bool m_isValid = false; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 059feabde..ca8ac1aa8 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -36,7 +36,6 @@ #include "WorldList.h" #include -#include #include #include #include @@ -65,9 +64,9 @@ void WorldList::startWatching() update(); m_isWatching = m_watcher->addPath(m_dir.absolutePath()); if (m_isWatching) { - qDebug() << "Started watching " << m_dir.absolutePath(); + qDebug() << "Started watching" << m_dir.absolutePath(); } else { - qDebug() << "Failed to start watching " << m_dir.absolutePath(); + qDebug() << "Failed to start watching" << m_dir.absolutePath(); } } @@ -78,9 +77,9 @@ void WorldList::stopWatching() } m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); if (!m_isWatching) { - qDebug() << "Stopped watching " << m_dir.absolutePath(); + qDebug() << "Stopped watching" << m_dir.absolutePath(); } else { - qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + qDebug() << "Failed to stop watching" << m_dir.absolutePath(); } } @@ -158,7 +157,8 @@ bool WorldList::resetIcon(int row) return false; World& m = m_worlds[row]; if (m.resetIcon()) { - emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); + QModelIndex modelIndex = index(row, NameColumn); + emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole }); return true; } return false; @@ -352,7 +352,7 @@ Qt::DropActions WorldList::supportedDropActions() const void WorldList::installWorld(QFileInfo filename) { - qDebug() << "installing: " << filename.absoluteFilePath(); + qDebug() << "installing:" << filename.absoluteFilePath(); World w(filename); if (!w.isValid()) { return; @@ -427,7 +427,7 @@ void WorldList::loadWorldsAsync() m_worlds[row].setSize(size); // Notify views - QModelIndex modelIndex = index(row); + QModelIndex modelIndex = index(row, SizeColumn); emit dataChanged(modelIndex, modelIndex, { SizeRole }); } }, diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 29a65e275..bfb350a63 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -303,7 +303,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); - xboxApiToken = tokenFromJSONV3(data, "xrp-main"); mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); } @@ -333,7 +332,6 @@ QJsonObject AccountData::saveState() const output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); - tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } else if (type == AccountType::Offline) { output["type"] = "Offline"; @@ -358,28 +356,10 @@ QString AccountData::profileId() const QString AccountData::profileName() const { if (minecraftProfile.name.size() == 0) { - return QObject::tr("No profile (%1)").arg(accountDisplayString()); - } else { - return minecraftProfile.name; + return QObject::tr("No Minecraft profile"); } -} -QString AccountData::accountDisplayString() const -{ - switch (type) { - case AccountType::Offline: { - return QObject::tr(""); - } - case AccountType::MSA: { - if (xboxApiToken.extra.contains("gtg")) { - return xboxApiToken.extra["gtg"].toString(); - } - return "Xbox profile missing"; - } - default: { - return "Invalid Account"; - } - } + return minecraftProfile.name; } QString AccountData::lastError() const diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 3b76012cb..96ef94f30 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -41,6 +41,7 @@ #include #include +#include #include enum class Validity { None, Assumed, Certain }; @@ -95,9 +96,6 @@ struct AccountData { QJsonObject saveState() const; bool resumeStateFromV3(QJsonObject data); - //! userName for Mojang accounts, gamertag for MSA - QString accountDisplayString() const; - //! Yggdrasil access token, as passed to the game. QString accessToken() const; @@ -111,7 +109,6 @@ struct AccountData { QString msaClientID; Token msaToken; Token userToken; - Token xboxApiToken; Token mojangservicesToken; Token yggdrasilToken; @@ -122,5 +119,6 @@ struct AccountData { // runtime only information (not saved with the account) QString internalId; QString errorString; + QNetworkReply::NetworkError networkError = QNetworkReply::NoError; AccountState accountState = AccountState::Unchecked; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index cef79d200..418ba98d2 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -168,6 +169,26 @@ void AccountList::removeAccount(QModelIndex index) } } +void AccountList::moveAccount(QModelIndex index, int delta) +{ + const int row = index.row(); + const int newRow = row + delta; + if (index.isValid() && row < m_accounts.size() && newRow >= 0 && newRow < m_accounts.size()) { + // Qt is stupid, https://doc.qt.io/qt-6/qabstractitemmodel.html#beginMoveRows + const int modelDestinationRow = (newRow > row) ? newRow + 1 : newRow; + + if (beginMoveRows(QModelIndex(), row, row, QModelIndex(), modelDestinationRow)) { + m_accounts.move(row, newRow); + endMoveRows(); + + onListChanged(); + } else { + qCritical().noquote() << "AccountList: failed to move account from" << row << "to" << newRow + << QString("(%1 accounts in total)").arg(this->count()); + } + } +} + MinecraftAccountPtr AccountList::defaultAccount() const { return m_defaultAccount; @@ -295,12 +316,28 @@ QVariant AccountList::data(const QModelIndex& index, int role) const MinecraftAccountPtr account = at(index.row()); switch (role) { + case Qt::SizeHintRole: + if (index.column() == ProfileNameColumn) { + return QSize(0, 30); + } + + return QVariant(); + case Qt::DecorationRole: + if (index.column() == ProfileNameColumn) { + auto face = account->getFace(24, 24); + + if (!face.isNull()) { + return face; + } else { + return QIcon::fromTheme("noaccount").pixmap(24, 24); + } + } + + return QVariant(); case Qt::DisplayRole: switch (index.column()) { case ProfileNameColumn: return account->profileName(); - case NameColumn: - return account->accountDisplayString(); case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { @@ -318,9 +355,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const return QVariant(); } - case Qt::ToolTipRole: - return account->accountDisplayString(); - case PointerRole: return QVariant::fromValue(account); @@ -341,8 +375,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o switch (section) { case ProfileNameColumn: return tr("Username"); - case NameColumn: - return tr("Account"); case TypeColumn: return tr("Type"); case StatusColumn: @@ -355,8 +387,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o switch (section) { case ProfileNameColumn: return tr("Minecraft username associated with the account."); - case NameColumn: - return tr("User name of the account."); case TypeColumn: return tr("Type of the account (MSA or Offline)"); case StatusColumn: @@ -420,7 +450,7 @@ bool AccountList::loadList() // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { - qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + qCritical() << QString("Failed to read the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } @@ -537,7 +567,7 @@ bool AccountList::saveList() // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::WriteOnly)) { - qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + qCritical() << QString("Failed to save the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } @@ -548,7 +578,7 @@ bool AccountList::saveList() qDebug() << "Saved account list to" << m_listFilePath; return true; } else { - qDebug() << "Failed to save accounts to" << m_listFilePath; + qDebug() << "Failed to save accounts to" << m_listFilePath << "error:" << file.errorString(); return false; } } @@ -574,7 +604,7 @@ void AccountList::fillQueue() if (m_defaultAccount && m_defaultAccount->shouldRefresh()) { auto idToRefresh = m_defaultAccount->internalId(); m_refreshQueue.push_back(idToRefresh); - qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + qDebug() << "AccountList: Queued default account with internal ID" << idToRefresh << "to refresh first"; } for (int i = 0; i < count(); i++) { @@ -598,7 +628,7 @@ void AccountList::requestRefresh(QString accountId) m_refreshQueue.removeAt(index); } m_refreshQueue.push_front(accountId); - qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue"; + qDebug() << "AccountList: Pushed account with internal ID" << accountId << "to the front of the queue"; if (!isActive()) { tryNext(); } @@ -610,7 +640,7 @@ void AccountList::queueRefresh(QString accountId) return; } m_refreshQueue.push_back(accountId); - qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh"; + qDebug() << "AccountList: Queued account with internal ID" << accountId << "to refresh"; } void AccountList::tryNext() @@ -618,21 +648,32 @@ void AccountList::tryNext() while (m_refreshQueue.length()) { auto accountId = m_refreshQueue.front(); m_refreshQueue.pop_front(); + bool found = false; for (int i = 0; i < count(); i++) { auto account = at(i); if (account->internalId() == accountId) { + found = true; + if (!account->shouldRefresh()) { + // Account no longer needs refreshing, skip it. + qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID" + << accountId << "(no longer needs refresh)"; + break; + } m_currentTask = account->refresh(); if (m_currentTask) { connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); m_currentTask->start(); - qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " + qDebug() << "RefreshSchedule: Processing account" << account->profileName() << "with internal ID" << accountId; return; } + break; } } - qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + if (!found) { + qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; + } } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); @@ -647,7 +688,7 @@ void AccountList::authSucceeded() void AccountList::authFailed(QString reason) { - qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + qDebug() << "RefreshSchedule: Background account refresh failed:" << reason; m_currentTask.reset(); m_nextTimer->start(1000 * 20); } diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 2f1276312..916f23341 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel { enum VListColumns { // TODO: Add icon column. ProfileNameColumn = 0, - NameColumn, TypeColumn, StatusColumn, @@ -78,6 +77,7 @@ class AccountList : public QAbstractListModel { void addAccount(MinecraftAccountPtr account); void removeAccount(QModelIndex index); + void moveAccount(QModelIndex index, int delta); int findAccountByProfileId(const QString& profileId) const; MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; QStringList profileNames() const; diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 287831b2f..5b8f98122 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -10,7 +10,6 @@ #include "minecraft/auth/steps/MSAStep.h" #include "minecraft/auth/steps/MinecraftProfileStep.h" #include "minecraft/auth/steps/XboxAuthorizationStep.h" -#include "minecraft/auth/steps/XboxProfileStep.h" #include "minecraft/auth/steps/XboxUserStep.h" #include "tasks/Task.h" @@ -32,11 +31,9 @@ AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) m_steps.append(oauthStep); } m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); m_steps.append( makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); @@ -69,6 +66,7 @@ void AuthFlow::nextStep() } m_currentStep = m_steps.front(); qDebug() << "AuthFlow:" << m_currentStep->describe(); + setStatus(m_currentStep->describe()); m_steps.pop_front(); connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); @@ -92,7 +90,9 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason) return true; } case AccountTaskState::STATE_WORKING: { - setStatus(m_currentStep ? m_currentStep->describe() : tr("Working...")); + if (!m_currentStep) { + setStatus(tr("Preparing to log in...")); + } m_data->accountState = AccountState::Working; return true; } @@ -148,8 +148,8 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason) } bool AuthFlow::abort() { - emitAborted(); if (m_currentStep) m_currentStep->abort(); + emitAborted(); return true; -} \ No newline at end of file +} diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index 710509d8e..d881a7691 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -2,7 +2,6 @@ #include #include -#include #include #include diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 3657befec..85d77be9c 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -20,23 +20,17 @@ QString AuthSession::serializeUserProperties() bool AuthSession::MakeOffline(QString offline_playername) { - if (status != PlayableOffline && status != PlayableOnline) { - return false; - } session = "-"; access_token = "0"; player_name = offline_playername; - status = PlayableOffline; return true; } void AuthSession::MakeDemo(QString name, QString u) { - wants_online = false; - demo = true; uuid = u; session = "-"; access_token = "0"; player_name = name; - status = PlayableOnline; // needs online to download the assets -}; \ No newline at end of file + launchMode = LaunchMode::Demo; +}; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index cbe604805..07db54213 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -3,6 +3,8 @@ #include #include +#include "LaunchMode.h" + class MinecraftAccount; struct AuthSession { @@ -11,16 +13,6 @@ struct AuthSession { QString serializeUserProperties(); - enum Status { - Undetermined, - RequiresOAuth, - RequiresPassword, - RequiresProfileSetup, - PlayableOffline, - PlayableOnline, - GoneOrMigrated - } status = Undetermined; - // combined session ID QString session; // volatile auth token @@ -29,15 +21,10 @@ struct AuthSession { QString player_name; // profile ID QString uuid; - // 'legacy' or 'mojang', depending on account type + // 'msa' or 'offline', depending on account type QString user_type; - // Did the auth server reply? - bool auth_server_online = false; - // Did the user request online mode? - bool wants_online = true; - - // Is this a demo session? - bool demo = false; + // the actual launch mode for this session + LaunchMode launchMode; }; using AuthSessionPtr = std::shared_ptr; diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index aaaec6e7f..f8131509f 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include #include "QObjectPtr.h" diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ca052c378..e346f015b 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,8 +55,7 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - static const QRegularExpression s_removeChars("[{}-]"); - data.internalId = QUuid::createUuid().toString().remove(s_removeChars); + data.internalId = QUuid::createUuid().toString(QUuid::Id128); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -77,15 +76,14 @@ 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(s_removeChars); - account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128); + account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -101,7 +99,7 @@ AccountState MinecraftAccount::accountState() const return data.accountState; } -QPixmap MinecraftAccount::getFace() const +QPixmap MinecraftAccount::getFace(int width, int height) const { QPixmap skinTexture; if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { @@ -112,7 +110,7 @@ QPixmap MinecraftAccount::getFace() const QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(64, 64, Qt::KeepAspectRatio); + return skin.scaled(width, height, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) @@ -193,6 +191,14 @@ void MinecraftAccount::authFailed(QString reason) emit activityChanged(false); } +QString MinecraftAccount::displayName() const +{ + if (const QList validStates{ AccountState::Unchecked, AccountState::Working, AccountState::Offline, AccountState::Online }; !validStates.contains(accountState())) { + return QString("⚠ %1").arg(profileName()); + } + return profileName(); +} + bool MinecraftAccount::isActive() const { return !m_currentTask.isNull(); @@ -235,17 +241,6 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { - static const QRegularExpression s_removeChars("[{}-]"); - if (ownsMinecraft() && !hasProfile()) { - session->status = AuthSession::RequiresProfileSetup; - } else { - if (session->wants_online) { - session->status = AuthSession::PlayableOnline; - } else { - session->status = AuthSession::PlayableOffline; - } - } - // volatile auth token session->access_token = data.accessToken(); // profile name @@ -253,7 +248,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); + session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { @@ -291,12 +286,12 @@ QUuid MinecraftAccount::uuidFromUsername(QString username) // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); - auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; - auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; - bAnd(digest, 6, (char)0x0f); // clear version - bOr(digest, 6, (char)0x30); // set to version 3 - bAnd(digest, 8, (char)0x3f); // clear variant - bOr(digest, 8, (char)0x80); // set to IETF variant + auto bOr = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] &= value; }; + bAnd(digest, 6, 0x0f); // clear version + bOr(digest, 6, 0x30); // set to version 3 + bAnd(digest, 8, 0x3f); // clear variant + bOr(digest, 8, 0x80); // set to IETF variant return QUuid::fromRfc4122(digest); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index a82d3f134..24608701d 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -104,14 +104,14 @@ class MinecraftAccount : public QObject, public Usable { public: /* queries */ QString internalId() const { return data.internalId; } - QString accountDisplayString() const { return data.accountDisplayString(); } - QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } QString profileName() const { return data.profileName(); } + QString displayName() const; + bool isActive() const; AccountType accountType() const noexcept { return data.type; } @@ -135,7 +135,7 @@ class MinecraftAccount : public QObject, public Usable { } } - QPixmap getFace() const; + QPixmap getFace(int width = 64, int height = 64) const; //! Returns the current state of the account AccountState accountState() const; diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index ba77d3e31..08c780694 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -75,7 +75,7 @@ bool getBool(QJsonValue value, bool& out) "Message":"", "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" } -// 2148916233 = missing XBox account +// 2148916233 = missing Xbox account // 2148916238 = child account not linked to a family */ @@ -86,7 +86,7 @@ bool parseXTokenResponse(QByteArray& data, Token& output, QString name) QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } @@ -123,7 +123,7 @@ bool parseXTokenResponse(QByteArray& data, Token& output, QString name) for (auto iter = obj_.begin(); iter != obj_.end(); iter++) { QString claim; if (!getString(obj_.value(iter.key()), claim)) { - qWarning() << "display claim " << iter.key() << " is not a string..."; + qWarning() << "display claim" << iter.key() << "is not a string..."; return false; } output.extra[iter.key()] = claim; @@ -148,7 +148,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } @@ -290,7 +290,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } @@ -331,7 +331,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) doc = QJsonDocument::fromJson(texturePayload, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } @@ -400,7 +400,7 @@ bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } @@ -463,7 +463,7 @@ bool parseMojangResponse(QByteArray& data, Token& output) qCDebug(authCredentials()) << data; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { - qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON:" << jsonError.errorString(); return false; } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index 5b9809c52..1a4e9aa74 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -23,35 +23,35 @@ QString EntitlementsStep::describe() void EntitlementsStep::perform() { - auto uuid = QUuid::createUuid(); - m_entitlements_request_id = uuid.toString().remove('{').remove('}'); + m_entitlements_request_id = QUuid::createUuid().toString(QUuid::WithoutBraces); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Download::makeByteArray(url); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting entitlements..."; } -void EntitlementsStep::onRequestDone() +void EntitlementsStep::onRequestDone(QByteArray* response) { - qCDebug(authCredentials()) << *m_response; + qCDebug(authCredentials()) << *response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement); + Parsers::parseMinecraftEntitlements(*response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h index f20fcac08..72f77dabe 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.h +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -18,11 +17,10 @@ class EntitlementsStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: QString m_entitlements_request_id; - std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index e067bc34c..7b26ca468 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -16,21 +16,22 @@ void GetSkinStep::perform() { QUrl url(m_data->minecraftProfile.skin.url); - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); + auto [request, response] = Net::Download::makeByteArray(url); + m_request = request; + m_request->enableAutoRetry(true); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); } -void GetSkinStep::onRequestDone() +void GetSkinStep::onRequestDone(QByteArray* response) { if (m_request->error() == QNetworkReply::NoError) - m_data->minecraftProfile.skin.data = *m_response; + m_data->minecraftProfile.skin.data = *response; emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h index c598f05d9..2cd74ab92 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.h +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -18,10 +17,9 @@ class GetSkinStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index 954f013af..4ceed8586 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -14,7 +14,7 @@ LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} QString LauncherLoginStep::describe() { - return tr("Accessing Mojang services."); + return tr("Fetching Minecraft access token"); } void LauncherLoginStep::perform() @@ -36,38 +36,40 @@ void LauncherLoginStep::perform() { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, requestBody.toUtf8()); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting Minecraft access token..."; } -void LauncherLoginStep::onRequestDone() +void LauncherLoginStep::onRequestDone(QByteArray* response) { - qCDebug(authCredentials()) << *m_response; + qCDebug(authCredentials()) << *response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); - if (Net::isApplicationError(m_request->error())) { + if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { + m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } return; } - if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) { + if (!Parsers::parseMojangResponse(*response, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; } - emit finished(AccountTaskState::STATE_WORKING, tr("")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft access token")); } diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h index 0b5969f2b..2501f5707 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.h +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" @@ -18,10 +17,9 @@ class LauncherLoginStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index 7a4722f21..78762a32a 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -66,15 +66,16 @@ void MSADeviceCodeStep::perform() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, payload); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); + connect(m_task.get(), &Task::finished, this, [this, response] { deviceAuthorizationFinished(response); }); m_task->start(); } @@ -110,22 +111,23 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d }; } -void MSADeviceCodeStep::deviceAuthorizationFinished() +void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) { - auto rsp = parseDeviceAuthorizationResponse(*m_response); + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { + qWarning() << "Device authorization failed:" << m_request->error() << m_request->errorString(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(m_request->errorString())); + return; + } + + auto rsp = parseDeviceAuthorizationResponse(*response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } - if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); - qDebug() << *m_response; - return; - } - if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { + qWarning() << "Device authorization failed: required fields missing"; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); return; } @@ -180,11 +182,11 @@ void MSADeviceCodeStep::authenticateUser() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, payload); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); - connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); + connect(m_request.get(), &Task::finished, this, [this, response] { authenticationFinished(response); }); m_request->setNetwork(APPLICATION->network()); m_request->start(); @@ -225,7 +227,7 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) obj.toVariantMap() }; } -void MSADeviceCodeStep::authenticationFinished() +void MSADeviceCodeStep::authenticationFinished(QByteArray* response) { if (m_request->error() == QNetworkReply::TimeoutError) { // rfc8628#section-3.5 @@ -237,7 +239,7 @@ void MSADeviceCodeStep::authenticationFinished() startPoolTimer(); return; } - auto rsp = parseAuthenticationResponse(*m_response); + auto rsp = parseAuthenticationResponse(*response); if (rsp.error == "slow_down") { // rfc8628#section-3.5 // "A variant of 'authorization_pending', the authorization request is @@ -272,5 +274,5 @@ void MSADeviceCodeStep::authenticationFinished() m_data->msaToken.extra = rsp.extra; m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; - emit finished(AccountTaskState::STATE_WORKING, tr("Got")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); } diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index 7f755563f..cfb8270d4 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -58,10 +58,10 @@ class MSADeviceCodeStep : public AuthStep { void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: - void deviceAuthorizationFinished(); + void deviceAuthorizationFinished(QByteArray* response); void startPoolTimer(); void authenticateUser(); - void authenticationFinished(); + void authenticationFinished(QByteArray* response); private: QString m_clientId; @@ -72,7 +72,6 @@ class MSADeviceCodeStep : public AuthStep { QTimer m_pool_timer; QTimer m_expiration_timer; - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index aa972be71..51a5e5ce0 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include @@ -56,13 +57,14 @@ bool isSchemeHandlerRegistered() process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); - return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME); + return output.contains(APPLICATION->desktopFileName()); #elif defined(Q_OS_WIN) QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); QSettings settings(regPath, QSettings::NativeFormat); - return settings.contains("shell/open/command/."); + const QString registeredRunCommand = settings.value("shell/open/command/.").toString().replace("\\", "/"); + return registeredRunCommand.contains(QCoreApplication::applicationFilePath()); #endif return true; } @@ -80,6 +82,33 @@ class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); } QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; } + + protected: + void networkReplyFinished(QNetworkReply* reply) override + { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "OAuth2 request failed:" << reply->readAll(); + } + + QOAuthOobReplyHandler::networkReplyFinished(reply); + } +}; + +class LoggingOAuthHttpServerReplyHandler final : public QOAuthHttpServerReplyHandler { + Q_OBJECT + + public: + explicit LoggingOAuthHttpServerReplyHandler(QObject* parent = nullptr) : QOAuthHttpServerReplyHandler(parent) {} + + protected: + void networkReplyFinished(QNetworkReply* reply) override + { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "OAuth2 request failed:" << reply->readAll(); + } + + QOAuthHttpServerReplyHandler::networkReplyFinished(reply); + } }; MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) @@ -88,7 +117,7 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { - auto replyHandler = new QOAuthHttpServerReplyHandler(this); + auto replyHandler = new LoggingOAuthHttpServerReplyHandler(this); replyHandler->setCallbackText(QString(R"XXX(