diff --git a/.clang-format b/.clang-format index 005a1006b..114bcba50 100644 --- a/.clang-format +++ b/.clang-format @@ -5,7 +5,6 @@ AllowShortIfStatementsOnASingleLine: false ColumnLimit: 140 --- Language: Cpp -AccessModifierOffset: -1 AlignConsecutiveMacros: None AlignConsecutiveAssignments: None BraceWrapping: @@ -16,4 +15,3 @@ BraceWrapping: BreakBeforeBraces: Custom BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false -QualifierAlignment: Left diff --git a/.clang-tidy b/.clang-tidy index c5eb09533..436dcf244 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,32 +1,5 @@ -FormatStyle: file - 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" + - modernize-use-using + - readability-avoid-const-params-in-decls -CheckOptions: - 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 +SystemHeaders: false diff --git a/.editorconfig b/.editorconfig index 03b99379b..56166b207 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,22 +1,8 @@ -# EditorConfig specs and documentation: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{yml,nix}] -indent_size = 2 - -# C++ Code Style settings -[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] -cpp_generate_documentation_comments = doxygen_slash_star - -[CMakeLists.txt] -ij_continuation_indent_size = 4 +# EditorConfig specs and documentation: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# C++ Code Style settings +[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] +cpp_generate_documentation_comments = doxygen_slash_star diff --git a/.envrc b/.envrc index 1d11c5354..190b5b2b3 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ -use nix +use flake watch_file nix/*.nix diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c7d36db27..528b128b1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,9 +5,3 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a - -# reformat codebase -1d468ac35ad88d8c77cc83f25e3704d9bd7df01b - -# format a part of codebase -5c8481a118c8fefbfe901001d7828eaf6866eac4 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3574bf20d..ea1fbfdd9 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: unconfirmed", "status: needs triage"] +labels: [bug] body: - type: markdown attributes: @@ -23,14 +23,14 @@ body: - macOS - Linux - Other -- type: input +- type: textarea 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: input +- type: textarea 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 5e6d68e65..fa7cdbe61 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: ["type: enhancement", "status: needs discussion", "status: needs triage"] +labels: [rfc] 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 18a202ae1..ddee86b65 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -1,6 +1,6 @@ name: Suggestion description: Make a suggestion -labels: ["type: enhancement", "status: needs triage"] +labels: [enhancement] body: - type: markdown attributes: diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml deleted file mode 100644 index 2ce6ca955..000000000 --- a/.github/actions/package/linux/action.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: Package for Linux -description: Create Linux packages for Prism Launcher - -inputs: - version: - description: Launcher version - required: true - build-type: - description: Type for the build - required: true - default: Debug - artifact-name: - description: Name of the uploaded artifact - required: true - default: Linux - qt-version: - description: Version of Qt to use - required: true - gpg-private-key: - description: Private key for AppImage signing - required: false - gpg-private-key-id: - description: ID for the gpg-private-key, to select the signing key - required: false - -runs: - using: composite - - steps: - - name: 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: | - # Fixup architecture naming for AppImages - dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" - case "$dpkg_arch" in - "amd64") - APPIMAGE_ARCH="x86_64" - ;; - "arm64") - APPIMAGE_ARCH="aarch64" - ;; - *) - echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" - exit 1 - ;; - esac - echo "APPIMAGE_ARCH=$APPIMAGE_ARCH" >> "$GITHUB_ENV" - - # Used for the file paths of libraries - echo "DEB_HOST_MULTIARCH=$(dpkg-architecture -q DEB_HOST_MULTIARCH)" >> "$GITHUB_ENV" - - - name: Package AppImage - shell: bash - env: - VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.version }} - BUILD_DIR: build - INSTALL_APPIMAGE_DIR: install-appdir - - GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }} - - if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then - echo "$GPG_PRIVATE_KEY" > privkey.asc - gpg --import privkey.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 - - sharun lib4bin \ - --hard-links \ - --with-hooks \ - --dst-dir "$INSTALL_APPIMAGE_DIR" \ - "$INSTALL_APPIMAGE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so - - cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/bin/ - # 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" \ - "$APPIMAGE_DEST" - - - name: Package portable tarball - shell: bash - env: - BUILD_DIR: build - - INSTALL_PORTABLE_DIR: install-portable - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - - sharun lib4bin \ - --with-hooks \ - --hard-links \ - --dst-dir "$INSTALL_PORTABLE_DIR" \ - "$INSTALL_PORTABLE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so - - # 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 -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@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@v7 - with: - name: 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@v7 - with: - name: 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 deleted file mode 100644 index 1af01250f..000000000 --- a/.github/actions/package/macos/action.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: Package for macOS -description: Create a macOS package for Prism Launcher - -inputs: - version: - description: Launcher version - required: true - build-type: - description: Type for the build - required: true - default: Debug - artifact-name: - description: Name of the uploaded artifact - required: true - default: macOS - apple-codesign-cert: - description: Certificate for signing macOS builds - required: false - apple-codesign-password: - description: Password for signing macOS builds - required: false - apple-codesign-id: - description: Certificate ID for signing macOS builds - required: false - apple-notarize-apple-id: - description: Apple ID used for notarizing macOS builds - required: false - apple-notarize-team-id: - description: Team ID used for notarizing macOS builds - required: false - apple-notarize-password: - description: Password used for notarizing macOS builds - required: false - sparkle-ed25519-key: - description: Private key for signing Sparkle updates - required: false - -runs: - using: composite - - steps: - - name: Fetch codesign certificate - shell: bash - run: | - echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12 - if [ -n '${{ inputs.apple-codesign-id }}' ]; then - security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain - security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.apple-codesign-password }}' build.keychain - else - echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY - fi - - - name: Package - shell: bash - env: - BUILD_DIR: build - INSTALL_DIR: install - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} - - cd ${{ env.INSTALL_DIR }} - chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" - - if [ -n '${{ inputs.apple-codesign-id }}' ]; then - APPLE_CODESIGN_ID='${{ inputs.apple-codesign-id }}' - ENTITLEMENTS_FILE='../program_info/App.entitlements' - else - APPLE_CODESIGN_ID='-' - ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements' - fi - - sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" - mv "PrismLauncher.app" "Prism Launcher.app" - - - name: Notarize - shell: bash - env: - INSTALL_DIR: install - run: | - cd ${{ env.INSTALL_DIR }} - - if [ -n '${{ inputs.apple-notarize-password }}' ]; then - ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - xcrun notarytool submit ../PrismLauncher.zip \ - --wait --progress \ - --apple-id '${{ inputs.apple-notarize-apple-id }}' \ - --team-id '${{ inputs.apple-notarize-team-id }}' \ - --password '${{ inputs.apple-notarize-password }}' - - xcrun stapler staple "Prism Launcher.app" - else - echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY - fi - ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - - - name: 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_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_zip\` (ZIP) - - :memo: Sparkle Signature (ed25519): \`$signature_dmg\` (DMG) - EOF - else - cat >> $GITHUB_STEP_SUMMARY << EOF - ### Artifact Information :information_source: - - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) - EOF - fi - - - name: Upload binary tarball - uses: actions/upload-artifact@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 deleted file mode 100644 index 532f3db44..000000000 --- a/.github/actions/package/windows/action.yml +++ /dev/null @@ -1,186 +0,0 @@ -name: Package for Windows -description: Create a Windows package for Prism Launcher - -inputs: - version: - description: Launcher version - required: true - build-type: - description: Type for the build - required: true - default: Debug - artifact-name: - description: Name of the uploaded artifact - required: true - msystem: - description: MSYS2 subsystem to use - required: false - azure-client-id: - description: Client ID for the Azure Signer Application - required: true - azure-tenant-id: - description: Tenant ID for the Azure Signer Application - required: true - azure-subscription-id: - description: Subscription ID for the Azure Signer Application - required: true - -runs: - using: composite - - steps: - - name: Package (MinGW) - if: ${{ inputs.msystem != '' }} - shell: msys2 {0} - env: - BUILD_DIR: build - INSTALL_DIR: install - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} - touch ${{ env.INSTALL_DIR }}/manifest.txt - for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Package (MSVC) - if: ${{ inputs.msystem == '' }} - shell: pwsh - env: - BUILD_DIR: build - INSTALL_DIR: install - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} - - cd ${{ github.workspace }} - - Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Emit warning for unsigned builds - 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: ${{ 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: ${{ 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-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 - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - - - name: Package (MinGW, portable) - if: ${{ inputs.msystem != '' }} - shell: msys2 {0} - env: - BUILD_DIR: build - INSTALL_DIR: install - INSTALL_PORTABLE_DIR: install-portable - run: | - cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - - - name: Package (MSVC, portable) - if: ${{ inputs.msystem == '' }} - shell: pwsh - env: - BUILD_DIR: build - INSTALL_DIR: install - INSTALL_PORTABLE_DIR: install-portable - run: | - cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - - Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Package (installer) - shell: pwsh - env: - BUILD_DIR: build - INSTALL_DIR: install - - NSCURL_VERSION: "v24.9.26.122" - NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - run: | - New-Item -Name NSISPlugins -ItemType Directory - Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip - $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash - if ( $nscurl_hash -ne "${{ env.nscurl_sha256 }}") { - echo "::error:: NSCurl.zip sha256 mismatch" - exit 1 - } - Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl - - cd ${{ env.INSTALL_DIR }} - makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - - - name: Sign installer - 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 }}\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 - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - - - name: Upload binary zip - 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@v7 - with: - name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} - path: install-portable/** - - - name: Upload installer - 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 deleted file mode 100644 index b73c7509a..000000000 --- a/.github/actions/setup-dependencies/action.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Setup Dependencies -description: Install and setup dependencies for building Prism Launcher - -inputs: - build-type: - description: Type for the build - required: true - default: Debug - artifact-name: - description: Name of the uploaded artifact - required: true - msystem: - description: MSYS2 subsystem to use - required: false - vcvars-arch: - description: Visual Studio architecture to use - required: false - qt-architecture: - description: Qt architecture - required: false - qt-version: - description: Version of Qt to use - required: true - -outputs: - build-type: - description: Type of build used - value: ${{ inputs.build-type }} - qt-version: - description: Version of Qt used - value: ${{ inputs.qt-version }} - -runs: - using: composite - - steps: - - name: Setup Linux dependencies - if: ${{ runner.os == 'Linux' }} - uses: ./.github/actions/setup-dependencies/linux - - - name: Setup macOS dependencies - if: ${{ runner.os == 'macOS' }} - uses: ./.github/actions/setup-dependencies/macos - with: - build-type: ${{ inputs.build-type }} - - - name: Setup Windows dependencies - if: ${{ runner.os == 'Windows' }} - uses: ./.github/actions/setup-dependencies/windows - with: - build-type: ${{ inputs.build-type }} - msystem: ${{ inputs.msystem }} - vcvars-arch: ${{ inputs.vcvars-arch }} - - # TODO(@getchoo): Get this working on MSYS2! - - name: Setup ccache - if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} - uses: hendrikmuhs/ccache-action@v1.2.23 - with: - variant: sccache - create-symlink: ${{ runner.os != 'Windows' }} - key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.artifact-name }}-sccache - - - name: Use ccache on debug builds - if: ${{ inputs.build-type == 'Debug' }} - shell: bash - env: - # Only use ccache on MSYS2 - CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem != '') && 'ccache' || 'sccache' }} - run: | - echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" - echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" - - - name: Install Qt - if: ${{ inputs.msystem == '' }} - uses: jurplel/install-qt-action@v4 - with: - aqtversion: "==3.1.*" - version: ${{ inputs.qt-version }} - modules: qtimageformats qtnetworkauth - cache: ${{ inputs.build-type == 'Debug' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml deleted file mode 100644 index fa5af702b..000000000 --- a/.github/actions/setup-dependencies/linux/action.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Setup Linux dependencies -description: Install and setup dependencies for building Prism Launcher - -runs: - using: composite - - steps: - - name: Install host dependencies - shell: bash - run: | - sudo apt-get -y update - sudo apt-get -y install \ - dpkg-dev \ - ninja-build extra-cmake-modules pkg-config scdoc \ - cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ - libxcb-cursor-dev libtomlplusplus-dev - - - name: Setup AppImage tooling - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - # Determinate AppImage architecture to use - dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" - case "$dpkg_arch" in - "amd64") - APPIMAGE_ARCH="x86_64" - ;; - "arm64") - APPIMAGE_ARCH="aarch64" - ;; - *) - echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" - exit 1 - ;; - esac - - gh release download \ - --repo VHSgunzo/sharun \ - --pattern "sharun-$APPIMAGE_ARCH-aio" \ - --output ~/bin/sharun - - # FIXME!: revert this to probonopd/go-appimage once https://github.com/probonopd/go-appimage/pull/377 is merged! - gh release download continuous \ - --repo DioEgizio/go-appimage \ - --pattern "mkappimage-*-$APPIMAGE_ARCH.AppImage" \ - --output ~/bin/mkappimage - - gh release download \ - --repo AppImageCommunity/AppImageUpdate \ - --pattern "AppImageUpdate-$APPIMAGE_ARCH.AppImage" \ - --output ~/bin/AppImageUpdate.AppImage - chmod +x ~/bin/* - echo "$HOME/bin" >> "$GITHUB_PATH" diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml deleted file mode 100644 index a90544be0..000000000 --- a/.github/actions/setup-dependencies/macos/action.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Setup macOS dependencies - -inputs: - build-type: - description: Type for the build - required: true - default: Debug - -runs: - using: composite - - steps: - - name: Install dependencies - shell: bash - run: | - brew update - brew install ninja extra-cmake-modules temurin@17 mono - - - name: Set JAVA_HOME - shell: bash - run: | - echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" - - - name: Setup vcpkg cache - if: ${{ inputs.build-type == 'Debug' }} - shell: bash - env: - USERNAME: ${{ github.repository_owner }} - GITHUB_TOKEN: ${{ github.token }} - FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json - run: | - mono `vcpkg fetch nuget | tail -n 1` \ - sources add \ - -Source "$FEED_URL" \ - -StorePasswordInClearText \ - -Name GitHubPackages \ - -UserName "$USERNAME" \ - -Password "$GITHUB_TOKEN" - mono `vcpkg fetch nuget | tail -n 1` \ - setapikey "$GITHUB_TOKEN" \ - -Source "$FEED_URL" - echo "VCPKG_BINARY_SOURCES=clear;nuget,$FEED_URL,readwrite" >> "$GITHUB_ENV" - - - name: Setup vcpkg environment - shell: bash - run: | - 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 deleted file mode 100644 index 24ad51d8f..000000000 --- a/.github/actions/setup-dependencies/windows/action.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Setup Windows Dependencies -description: Install and setup dependencies for building Prism Launcher - -inputs: - build-type: - description: Type for the build - required: true - default: Debug - msystem: - description: MSYS2 subsystem to use - required: false - vcvars-arch: - description: Visual Studio architecture to use - required: true - default: amd64 - -runs: - using: composite - - steps: - # NOTE: Installed on MinGW as well for SignTool - - name: Enter VS Developer shell - if: ${{ runner.os == 'Windows' }} - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: ${{ inputs.vcvars-arch }} - vsversion: 2022 - - - name: Setup Java (MSVC) - uses: actions/setup-java@v5 - with: - # NOTE(@getchoo): We should probably stay on Zulu. - # Temurin doesn't have Java 17 builds for WoA - distribution: zulu - java-version: 17 - - - name: Setup vcpkg cache (MSVC) - if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} - shell: pwsh - env: - USERNAME: ${{ github.repository_owner }} - GITHUB_TOKEN: ${{ github.token }} - FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json - run: | - .$(vcpkg fetch nuget) ` - sources add ` - -Source "$env:FEED_URL" ` - -StorePasswordInClearText ` - -Name GitHubPackages ` - -UserName "$env:USERNAME" ` - -Password "$env:GITHUB_TOKEN" - .$(vcpkg fetch nuget) ` - setapikey "$env:GITHUB_TOKEN" ` - -Source "$env:FEED_URL" - "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" | Out-File -Append $env:GITHUB_ENV - - - name: Setup vcpkg environment (MSVC) - if: ${{ inputs.msystem == '' }} - shell: bash - run: | - echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" - - - name: Setup MSYS2 (MinGW) - if: ${{ inputs.msystem != '' }} - uses: msys2/setup-msys2@v2 - with: - msystem: ${{ inputs.msystem }} - update: true - install: >- - git - pacboy: >- - toolchain:p - ccache:p - cmake:p - extra-cmake-modules:p - ninja:p - qt6-base:p - qt6-svg:p - qt6-imageformats:p - qt6-networkauth:p - cmark:p - qrencode:p - tomlplusplus:p - libarchive:p - - - name: List pacman packages (MinGW) - if: ${{ inputs.msystem != '' }} - shell: msys2 {0} - run: | - pacman -Qe - - - name: Retrieve ccache cache (MinGW) - if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - uses: actions/cache@v5.0.5 - with: - path: '${{ github.workspace }}\.ccache' - key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-mingw-w64-ccache - - - name: Setup ccache (MinGW) - if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - shell: msys2 {0} - run: | - ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' - ccache --set-config=max_size='500M' - ccache --set-config=compression=true - ccache -p # Show config diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 9cde3307d..4146cddf4 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -8,7 +8,8 @@ on: # the GitHub repository. This means that it should not evaluate user input in a # way that allows code injection. -permissions: {} +permissions: + contents: read jobs: backport: @@ -18,13 +19,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-slim + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v4.5 + uses: korthout/backport-action@v3.1.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml deleted file mode 100644 index 001080154..000000000 --- a/.github/workflows/blocked-prs.yml +++ /dev/null @@ -1,257 +0,0 @@ -name: Blocked/Stacked Pull Requests Automation - -on: - pull_request_target: - types: - - opened - - reopened - - edited - - synchronize - workflow_dispatch: - inputs: - pr_id: - description: Local Pull Request number to work on - required: true - type: number - -permissions: {} - -jobs: - blocked_status: - name: Check Blocked Status - runs-on: ubuntu-slim - - steps: - - name: Generate token - id: generate-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ vars.PULL_REQUEST_APP_ID }} - private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} - - - name: Setup From Dispatch Event - if: github.event_name == 'workflow_dispatch' - id: dispatch_event_setup - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - PR_NUMBER: ${{ inputs.pr_id }} - run: | - # setup env for the rest of the workflow - OWNER=$(dirname "${{ github.repository }}") - REPO=$(basename "${{ github.repository }}") - PR_JSON=$( - gh api \ - -H "Accept: application/vnd.github.raw+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/$OWNER/$REPO/pulls/$PR_NUMBER" - ) - echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV" - - - name: Setup Environment - id: env_setup - env: - EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }} - run: | - # setup env for the rest of the workflow - PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"} - { - echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")" - echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")" - echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")" - echo "JOB_DATA=$(jq -c ' - { - "repo": .base.repo.name, - "owner": .base.repo.owner.login, - "repoUrl": .base.repo.html_url, - "prNumber": .number, - "prHeadSha": .head.sha, - "prHeadLabel": .head.label, - "prBody": (.body // ""), - "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) - } - ' <<< "$PR_JSON")" - } >> "$GITHUB_ENV" - - - - name: Find Blocked/Stacked PRs in body - id: pr_ids - run: | - prs=$( - jq -c ' - .prBody as $body - | ( - $body | - reduce ( - . | scan("[Bb]locked (?:[Bb]y|[Oo]n):? #([0-9]+)") - | map({ - "type": "Blocked on", - "number": ( . | tonumber ) - }) - ) as $i ([]; . + [$i[]]) - ) as $bprs - | ( - $body | - reduce ( - . | scan("[Ss]tacked [Oo]n:? #([0-9]+)") - | map({ - "type": "Stacked on", - "number": ( . | tonumber ) - }) - ) as $i ([]; . + [$i[]]) - ) as $sprs - | ($bprs + $sprs) as $prs - | { - "blocking": $prs, - "numBlocking": ( $prs | length), - } - ' <<< "$JOB_DATA" - ) - echo "prs=$prs" >> "$GITHUB_OUTPUT" - - - name: Collect Blocked PR Data - id: blocking_data - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }} - run: | - blocked_pr_data=$( - while read -r pr_data ; do - gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \ - | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \ - ' - . | { - "type": $type, - "number": .number, - "merged": .merged, - "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end), - "labels": (reduce .labels[].name as $l ([]; . + [$l])), - "basePrUrl": .html_url, - "baseRepoName": .head.repo.name, - "baseRepoOwner": .head.repo.owner.login, - "baseRepoUrl": .head.repo.html_url, - "baseSha": .head.sha, - "baseRefName": .head.ref, - } - ' - done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s - ) - { - echo "data=$blocked_pr_data"; - echo "all_merged=$(jq -r 'all(.[] | (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")); .)' <<< "$blocked_pr_data")"; - echo "current_blocking=$(jq -c 'map( - select( - (.type == "Stacked on" and (.merged | not)) or - (.type == "Blocked on" and (.state == "Open")) - ) | .number - )' <<< "$blocked_pr_data" )"; - } >> "$GITHUB_OUTPUT" - - - name: Add 'blocked' Label if Missing - id: label_blocked - if: "(fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, '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 'status: blocked' "$PR_NUMBER" - - - name: Remove 'blocked' Label if All Dependencies Are Merged - id: unlabel_blocked - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged) - continue-on-error: true - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - run: | - gh -R ${{ github.repository }} issue edit --remove-label 'status: blocked' "$PR_NUMBER" - - - name: Apply 'blocking' Label to Unmerged Dependencies - id: label_blocking - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 - continue-on-error: true - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }} - run: | - while read -r pr ; do - gh -R ${{ github.repository }} issue edit --add-label 'status: blocking' "$pr" || true - done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") - - - name: Apply Blocking PR Status Check - id: blocked_check - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 - continue-on-error: true - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} - run: | - pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA") - # create commit Status, overwrites previous identical context - while read -r pr_data ; do - DESC=$( - jq -r 'if .type == "Stacked on" then - "Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged" - else - "Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed" - end ' <<< "$pr_data" - ) - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ - -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \ - -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ - -f "description=$DESC" \ - -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" - done < <(jq -c '.[]' <<< "$BLOCKING_DATA") - - - name: Context Comment - id: generate-comment - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 - continue-on-error: true - env: - BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} - run: | - COMMENT_PATH="$(pwd)/temp_comment_file.txt" - echo '

PR Dependencies :pushpin:

' > "$COMMENT_PATH" - echo >> "$COMMENT_PATH" - pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA") - while read -r pr_data ; do - base_pr=$(jq -r '.number' <<< "$pr_data") - base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data") - base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") - base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data") - compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label" - status=$(jq -r ' - if .type == "Stacked on" then - if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end - else - if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end - end - ' <<< "$pr_data") - type=$(jq -r '.type' <<< "$pr_data") - echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" - done < <(jq -c '.[]' <<< "$BLOCKING_DATA") - - { - echo 'body<> "$GITHUB_OUTPUT" - - - name: 💬 PR Comment - if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 - continue-on-error: true - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - COMMENT_BODY: ${{ steps.generate-comment.outputs.body }} - run: | - gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \ - --body "$COMMENT_BODY" \ - --create-if-none \ - --edit-last - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0596906c8..991ddcd8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,188 +1,704 @@ name: Build -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - on: - merge_group: - types: [checks_requested] - pull_request: workflow_call: inputs: - build-type: - description: Type of build (Debug or Release) + build_type: + description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) type: string default: Debug - environment: - description: Deployment environment to run under + is_qt_cached: + description: Enable Qt caching or not type: string - workflow_dispatch: - inputs: - build-type: - description: Type of build (Debug or Release) - type: string - default: Debug - -permissions: {} + default: true + secrets: + SPARKLE_ED25519_KEY: + description: Private key for signing Sparkle updates + required: false + WINDOWS_CODESIGN_CERT: + description: Certificate for signing Windows builds + required: false + WINDOWS_CODESIGN_PASSWORD: + description: Password for signing Windows builds + required: false + APPLE_CODESIGN_CERT: + description: Certificate for signing macOS builds + required: false + APPLE_CODESIGN_PASSWORD: + description: Password for signing macOS builds + required: false + APPLE_CODESIGN_ID: + description: Certificate ID for signing macOS builds + required: false + APPLE_NOTARIZE_APPLE_ID: + description: Apple ID used for notarizing macOS builds + required: false + APPLE_NOTARIZE_TEAM_ID: + description: Team ID used for notarizing macOS builds + required: false + APPLE_NOTARIZE_PASSWORD: + description: Password used for notarizing macOS builds + required: false + CACHIX_AUTH_TOKEN: + description: Private token for authenticating against Cachix cache + required: false + GPG_PRIVATE_KEY: + description: Private key for AppImage signing + required: false + GPG_PRIVATE_KEY_ID: + description: ID for the GPG_PRIVATE_KEY, to select the signing key + required: false jobs: build: - name: Build (${{ matrix.artifact-name }}) - - environment: ${{ inputs.environment || '' }} - - permissions: - contents: read - # Required for Azure Trusted Signing - id-token: write - # Required for vcpkg binary cache - packages: write - strategy: fail-fast: false matrix: include: - - os: ubuntu-24.04 - artifact-name: Linux - cmake-preset: linux - qt-version: 6.10.2 + - os: ubuntu-20.04 + qt_ver: 5 + qt_host: linux + qt_arch: "" + qt_version: "5.15.2" + qt_modules: "qtnetworkauth" - - os: ubuntu-24.04-arm - artifact-name: Linux-aarch64 - cmake-preset: linux - qt-version: 6.10.2 + - os: ubuntu-20.04 + qt_ver: 6 + qt_host: linux + qt_arch: "" + qt_version: "6.5.3" + qt_modules: "qt5compat qtimageformats qtnetworkauth" - os: windows-2022 - artifact-name: Windows-MinGW-w64 - cmake-preset: windows_mingw - msystem: CLANG64 - vcvars-arch: amd64_x86 - - - os: windows-11-arm - artifact-name: Windows-MinGW-arm64 - cmake-preset: windows_mingw - msystem: CLANGARM64 - vcvars-arch: arm64 + name: "Windows-MinGW-w64" + msystem: clang64 + vcvars_arch: "amd64_x86" - os: windows-2022 - artifact-name: Windows-MSVC - 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.2 + name: "Windows-MSVC" + msystem: "" + architecture: "x64" + vcvars_arch: "amd64" + qt_ver: 6 + qt_host: windows + qt_arch: "" + qt_version: "6.7.3" + qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - - os: windows-11-arm - artifact-name: Windows-MSVC-arm64 - cmake-preset: windows_msvc - vcvars-arch: arm64 - qt-version: 6.10.2 + - os: windows-2022 + name: "Windows-MSVC-arm64" + msystem: "" + architecture: "arm64" + vcvars_arch: "amd64_arm64" + qt_ver: 6 + qt_host: windows + qt_arch: "win64_msvc2019_arm64" + qt_version: "6.7.3" + qt_modules: "qt5compat qtimageformats qtnetworkauth" + nscurl_tag: "v24.9.26.122" + nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" - - os: macos-26 - artifact-name: macOS - cmake-preset: macos_universal - macosx-deployment-target: 12.0 - qt-version: 6.9.3 + - os: macos-14 + name: macOS + macosx_deployment_target: 11.0 + qt_ver: 6 + qt_host: mac + qt_arch: "" + qt_version: "6.7.3" + qt_modules: "qt5compat qtimageformats qtnetworkauth" + + - os: macos-14 + name: macOS-Legacy + macosx_deployment_target: 10.13 + qt_ver: 5 + qt_host: mac + qt_version: "5.15.2" + qt_modules: "qtnetworkauth" runs-on: ${{ matrix.os }} - defaults: - run: - shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} - env: - ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 - BUILD_PLATFORM: official - BUILD_TYPE: ${{ inputs.build-type || 'Debug' }} - CMAKE_PRESET: ${{ matrix.cmake-preset }} - - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} + INSTALL_DIR: "install" + INSTALL_PORTABLE_DIR: "install-portable" + INSTALL_APPIMAGE_DIR: "install-appdir" + BUILD_DIR: "build" + CCACHE_VAR: "" + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 steps: ## - # SETUP + # PREPARE + ## + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: "true" + + - name: "Setup MSYS2" + if: runner.os == 'Windows' && matrix.msystem != '' + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msystem }} + update: true + install: >- + git + mingw-w64-x86_64-binutils + pacboy: >- + toolchain:p + cmake:p + extra-cmake-modules:p + ninja:p + qt6-base:p + qt6-svg:p + qt6-imageformats:p + quazip-qt6:p + ccache:p + qt6-5compat:p + qt6-networkauth:p + cmark:p + + - name: Force newer ccache + if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' + run: | + choco install ccache --version 4.7.1 + + - name: Setup ccache + if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' + uses: hendrikmuhs/ccache-action@v1.2.14 + with: + key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} + + - name: Retrieve ccache cache (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' + uses: actions/cache@v4.2.0 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} + restore-keys: | + ${{ matrix.os }}-mingw-w64-ccache + + - name: Setup ccache (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' + shell: msys2 {0} + run: | + ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' + ccache --set-config=max_size='500M' + ccache --set-config=compression=true + ccache -p # Show config + ccache -z # Zero stats + + - name: Use ccache on Debug builds only + if: inputs.build_type == 'Debug' + shell: bash + run: | + echo "CCACHE_VAR=ccache" >> $GITHUB_ENV + + - name: Set short version + shell: bash + run: | + ver_short=`git rev-parse --short HEAD` + echo "VERSION=$ver_short" >> $GITHUB_ENV + + - name: Install Dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get -y update + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev + + - name: Install Dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew update + brew install ninja extra-cmake-modules + + - name: Install host Qt (Windows MSVC arm64) + if: runner.os == 'Windows' && matrix.architecture == 'arm64' + uses: jurplel/install-qt-action@v3 + with: + aqtversion: "==3.1.*" + py7zrversion: ">=0.20.2" + version: ${{ matrix.qt_version }} + host: "windows" + target: "desktop" + arch: "" + modules: ${{ matrix.qt_modules }} + cache: ${{ inputs.is_qt_cached }} + cache-key-prefix: host-qt-arm64-windows + dir: ${{ github.workspace }}\HostQt + set-env: false + + - name: Install Qt (macOS, Linux & Windows MSVC) + if: matrix.msystem == '' + uses: jurplel/install-qt-action@v3 + with: + aqtversion: "==3.1.*" + py7zrversion: ">=0.20.2" + version: ${{ matrix.qt_version }} + target: "desktop" + arch: ${{ matrix.qt_arch }} + modules: ${{ matrix.qt_modules }} + tools: ${{ matrix.qt_tools }} + cache: ${{ inputs.is_qt_cached }} + + - name: Install MSVC (Windows MSVC) + if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool + uses: ilammy/msvc-dev-cmd@v1 + with: + vsversion: 2022 + arch: ${{ matrix.vcvars_arch }} + + - name: Prepare AppImage (Linux) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + run: | + wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" + wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" + wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" + + wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" + + sudo apt install libopengl0 + + - name: Add QT_HOST_PATH var (Windows MSVC arm64) + if: runner.os == 'Windows' && matrix.architecture == 'arm64' + run: | + echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV + + - name: Setup java (macOS) + if: runner.os == 'macOS' + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + ## + # CONFIGURE ## - - name: Checkout - uses: actions/checkout@v6 - with: - submodules: true + - name: Configure CMake (macOS) + if: runner.os == 'macOS' && matrix.qt_ver == 6 + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja - - name: Setup dependencies - id: setup-dependencies - uses: ./.github/actions/setup-dependencies - with: - build-type: ${{ env.BUILD_TYPE }} - artifact-name: ${{ matrix.artifact-name }} - msystem: ${{ matrix.msystem }} - vcvars-arch: ${{ matrix.vcvars-arch }} - qt-version: ${{ matrix.qt-version }} + - name: Configure CMake (macOS-Legacy) + if: runner.os == 'macOS' && matrix.qt_ver == 5 + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -DCMAKE_OSX_ARCHITECTURES="x86_64" -G Ninja + + - name: Configure CMake (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja + + - name: Configure CMake (Windows MSVC) + if: runner.os == 'Windows' && matrix.msystem == '' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} + # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) + if ("${{ env.CCACHE_VAR }}") + { + Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe + echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV + echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV + echo "TrackFileAccess=false" >> $env:GITHUB_ENV + } + # Needed for ccache, but also speeds up compile + echo "UseMultiToolTask=true" >> $env:GITHUB_ENV + + - name: Configure CMake (Linux) + if: runner.os == 'Linux' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja ## # BUILD ## - - name: Configure project + - name: Build + if: runner.os != 'Windows' run: | - cmake --preset "$CMAKE_PRESET" + cmake --build ${{ env.BUILD_DIR }} - - name: Run build + - name: Build (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} run: | - cmake --build --preset "$CMAKE_PRESET" --config "$BUILD_TYPE" + cmake --build ${{ env.BUILD_DIR }} - - name: Run tests + - name: Build (Windows MSVC) + if: runner.os == 'Windows' && matrix.msystem == '' run: | - ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE" + cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} ## - # PACKAGE + # TEST ## - - name: Get short version - id: short-version - shell: bash + - name: Test + if: runner.os != 'Windows' run: | - echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + ctest -E "^example64|example$" --test-dir build --output-on-failure - - name: Package (Linux) - if: ${{ runner.os == 'Linux' }} - uses: ./.github/actions/package/linux - with: - version: ${{ steps.short-version.outputs.version }} - build-type: ${{ steps.setup-dependencies.outputs.build-type }} - artifact-name: ${{ matrix.artifact-name }} - qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} + - name: Test (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + ctest -E "^example64|example$" --test-dir build --output-on-failure - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }} + - name: Test (Windows MSVC) + if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' + run: | + ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} + + ## + # PACKAGE BUILDS + ## + + - name: Fetch codesign certificate (macOS) + if: runner.os == 'macOS' + run: | + echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12 + if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then + security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain + else + echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY + fi - name: Package (macOS) - if: ${{ runner.os == 'macOS' }} - uses: ./.github/actions/package/macos - with: - version: ${{ steps.short-version.outputs.version }} - build-type: ${{ steps.setup-dependencies.outputs.build-type }} - artifact-name: ${{ matrix.artifact-name }} + if: runner.os == 'macOS' + run: | + cmake --install ${{ env.BUILD_DIR }} - apple-codesign-cert: ${{ secrets.APPLE_CODESIGN_CERT }} - apple-codesign-password: ${{ secrets.APPLE_CODESIGN_PASSWORD }} - apple-codesign-id: ${{ secrets.APPLE_CODESIGN_ID }} - apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} - apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} - apple-notarize-password: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} - sparkle-ed25519-key: ${{ secrets.SPARKLE_ED25519_KEY }} + cd ${{ env.INSTALL_DIR }} + chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" - - name: Package (Windows) - if: ${{ runner.os == 'Windows' }} - uses: ./.github/actions/package/windows + if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then + APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}' + else + APPLE_CODESIGN_ID='-' + fi + + sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" + mv "PrismLauncher.app" "Prism Launcher.app" + + - name: Notarize (macOS) + if: runner.os == 'macOS' + run: | + cd ${{ env.INSTALL_DIR }} + + if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + xcrun notarytool submit ../PrismLauncher.zip \ + --wait --progress \ + --apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \ + --team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \ + --password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' + + xcrun stapler staple "Prism Launcher.app" + else + echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY + fi + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + + - name: Make Sparkle signature (macOS) + if: matrix.name == 'macOS' + run: | + if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then + echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem + signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + rm ed25519-priv.pem + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Artifact Information :information_source: + - :memo: Sparkle Signature (ed25519): \`$signature\` + EOF + else + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Artifact Information :information_source: + - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) + EOF + fi + + - name: Package (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + cmake --install ${{ env.BUILD_DIR }} + touch ${{ env.INSTALL_DIR }}/manifest.txt + for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (Windows MSVC) + if: runner.os == 'Windows' && matrix.msystem == '' + run: | + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} + + cd ${{ github.workspace }} + + Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Fetch codesign certificate (Windows) + if: runner.os == 'Windows' + shell: bash # yes, we are not using MSYS2 or PowerShell here + run: | + echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx + + - name: Sign executable (Windows) + if: runner.os == 'Windows' + run: | + if (Get-Content ./codesign.pfx){ + cd ${{ env.INSTALL_DIR }} + # We ship the exact same executable for portable and non-portable editions, so signing just once is fine + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Package (Windows MinGW-w64, portable) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + + - name: Package (Windows MSVC, portable) + if: runner.os == 'Windows' && matrix.msystem == '' + run: | + cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + + Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (Windows, installer) + if: runner.os == 'Windows' + run: | + if ('${{ matrix.nscurl_tag }}') { + New-Item -Name NSISPlugins -ItemType Directory + Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip + $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash + if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") { + echo "::error:: NSCurl.zip sha256 mismatch" + exit 1 + } + Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl + } + cd ${{ env.INSTALL_DIR }} + makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" + + - name: Sign installer (Windows) + if: runner.os == 'Windows' + run: | + if (Get-Content ./codesign.pfx){ + SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Package AppImage (Linux) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + shell: bash env: - 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 }} - artifact-name: ${{ matrix.artifact-name }} - msystem: ${{ matrix.msystem }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml + export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated + + export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" + + chmod +x linuxdeploy-*.AppImage + + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" + export LD_LIBRARY_PATH + + chmod +x AppImageUpdate-x86_64.AppImage + cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin + + export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" + + if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then + export SIGN=1 + export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }} + mkdir -p ~/.gnupg/ + echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key + gpg --import ~/.gnupg/private.key + else + echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY + fi + + ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg + + mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" + + - name: Package (Linux, portable) + if: runner.os == 'Linux' + run: | + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja + cmake --install ${{ env.BUILD_DIR }} + cmake --install ${{ env.BUILD_DIR }} --component portable + + mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib + mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib + + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + cd ${{ env.INSTALL_PORTABLE_DIR }} + tar -czf ../PrismLauncher-portable.tar.gz * + + ## + # UPLOAD BUILDS + ## + + - name: Upload binary tarball (macOS) + if: runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} + path: PrismLauncher.zip + + - name: Upload binary zip (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} + path: ${{ env.INSTALL_DIR }}/** + + - name: Upload binary zip (Windows, portable) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: ${{ env.INSTALL_PORTABLE_DIR }}/** + + - name: Upload installer (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} + path: PrismLauncher-Setup.exe + + - name: Upload binary tarball (Linux, portable, Qt 5) + if: runner.os == 'Linux' && matrix.qt_ver != 6 + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: PrismLauncher-portable.tar.gz + + - name: Upload binary tarball (Linux, portable, Qt 6) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + path: PrismLauncher-portable.tar.gz + + - name: Upload AppImage (Linux) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + + - name: Upload AppImage Zsync (Linux) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync + path: PrismLauncher-Linux-x86_64.AppImage.zsync + + - name: ccache stats (Windows MinGW-w64) + if: runner.os == 'Windows' && matrix.msystem != '' + shell: msys2 {0} + run: | + ccache -s + + flatpak: + runs-on: ubuntu-latest + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8 + options: --privileged + steps: + - name: Checkout + uses: actions/checkout@v4 + if: inputs.build_type == 'Debug' + with: + submodules: true + + - name: Set short version + shell: bash + run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Build Flatpak (Linux) + if: inputs.build_type == 'Debug' + uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak + manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml + + nix: + name: Nix (${{ matrix.system }}) + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + system: x86_64-linux + + - os: macos-13 + system: x86_64-darwin + + - os: macos-14 + system: aarch64-darwin + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v30 + + # For PRs + - name: Setup Nix Magic Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + + # For in-tree builds + - name: Setup Cachix + uses: cachix/cachix-action@v15 + with: + name: prismlauncher + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Run flake checks + run: | + nix flake check --print-build-logs --show-trace + + - name: Build debug package + if: ${{ inputs.build_type == 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher-debug + + - name: Build release package + if: ${{ inputs.build_type != 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml deleted file mode 100644 index d72994c50..000000000 --- a/.github/workflows/clang-tidy.yml +++ /dev/null @@ -1,48 +0,0 @@ -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 f9705bf53..5255f865b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,52 +1,35 @@ name: "CodeQL Code Scanning" -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - merge_group: - types: [checks_requested] - pull_request: - workflow_dispatch: - -permissions: {} +on: [ push, pull_request, workflow_dispatch ] jobs: CodeQL: runs-on: ubuntu-latest - - permissions: - contents: read - security-events: write - + steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: - submodules: "true" + submodules: 'true' - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml queries: security-and-quality languages: cpp, java - - name: Setup dependencies - uses: ./.github/actions/setup-dependencies - with: - build-type: Debug - qt-version: 6.4.3 + - name: Install Dependencies + run: + sudo apt-get -y update + + sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev - name: Configure and Build run: | - cmake --preset linux -DLauncher_USE_PCH=OFF - cmake --build --preset linux --config Debug + cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja - - name: Run tests - run: | - ctest --preset linux --build-config Debug --extra-verbose --output-on-failure + cmake --build build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml deleted file mode 100644 index 7af2c1ccb..000000000 --- a/.github/workflows/container.yml +++ /dev/null @@ -1,174 +0,0 @@ -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/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml deleted file mode 100644 index 3542a470e..000000000 --- a/.github/workflows/merge-blocking-pr.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Merged Blocking Pull Request Automation - -on: - pull_request_target: - types: - - closed - workflow_dispatch: - inputs: - pr_id: - description: Local Pull Request number to work on - required: true - type: number - -permissions: {} - -jobs: - update-blocked-status: - name: Update Blocked Status - 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, 'status: blocking') }}" - - steps: - - name: Generate token - id: generate-token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ vars.PULL_REQUEST_APP_ID }} - private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} - - - name: Gather Dependent PRs - id: gather_deps - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} - run: | - blocked_prs=$( - gh -R ${{ github.repository }} pr list --label 'status: blocked' --json 'number,body' \ - | jq -c --argjson pr "$PR_NUMBER" ' - reduce ( .[] | select( - .body | - scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | - map(tonumber) | - any(.[]; . == $pr) - )) as $i ([]; . + [$i]) - ' - ) - { - echo "deps=$blocked_prs" - echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" - } >> "$GITHUB_OUTPUT" - - - name: Trigger Blocked PR Workflows for Dependants - if: fromJSON(steps.gather_deps.outputs.numdeps) > 0 - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - DEPS: ${{ steps.gather_deps.outputs.deps }} - run: | - while read -r pr ; do - gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr" - done < <(jq -c '.[].number' <<< "$DEPS") - diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 58f4d263a..000000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Nix - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - push: - branches: - - "develop" - - "release-*" - tags: - - "*" - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - "**.md" - - # Build files - - "**.nix" - - "nix/**" - - "flake.lock" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - # Workflows - - ".github/workflows/nix.yml" - pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - "**.md" - - # Build files - - "**.nix" - - "nix/**" - - "flake.lock" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - # Workflows - - ".github/workflows/nix.yml" - workflow_dispatch: - -permissions: {} - -env: - DEBUG: ${{ github.ref_type != 'tag' }} - -jobs: - build: - name: Build (${{ matrix.system }}) - - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-22.04 - system: x86_64-linux - - - os: ubuntu-22.04-arm - system: aarch64-linux - - - os: macos-26 - system: aarch64-darwin - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Install Nix - uses: cachix/install-nix-action@v31 - - # For PRs - - name: Setup Nix Magic Cache - if: ${{ github.event_name == 'pull_request' }} - uses: DeterminateSystems/magic-nix-cache-action@v14 - with: - diagnostic-endpoint: "" - use-flakehub: false - - # For in-tree builds - - name: Setup Cachix - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - uses: cachix/cachix-action@v17 - with: - name: prismlauncher - authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - - - name: Run Flake checks - run: | - nix flake check --print-build-logs --show-trace - - - name: Build debug package - if: ${{ env.DEBUG == 'true' }} - run: | - nix build \ - --no-link --print-build-logs --print-out-paths \ - .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY" - - - name: Build release package - if: ${{ env.DEBUG == 'false' }} - env: - TAG: ${{ github.ref_name }} - SYSTEM: ${{ matrix.system }} - run: | - nix build --no-link --print-out-paths .#prismlauncher \ - | tee -a "$GITHUB_STEP_SUMMARY" \ - | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM" diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml new file mode 100644 index 000000000..0b8386d69 --- /dev/null +++ b/.github/workflows/trigger_builds.yml @@ -0,0 +1,43 @@ +name: Build Application + +on: + push: + branches-ignore: + - "renovate/**" + paths-ignore: + - "**.md" + - "**/LICENSE" + - "flake.lock" + - "packages/**" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + pull_request: + paths-ignore: + - "**.md" + - "**/LICENSE" + - "flake.lock" + - "packages/**" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" + workflow_dispatch: + +jobs: + build_debug: + name: Build Debug + uses: ./.github/workflows/build.yml + with: + build_type: Debug + is_qt_cached: true + secrets: + SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} + WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} + WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} + APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/release.yml b/.github/workflows/trigger_release.yml similarity index 62% rename from .github/workflows/release.yml rename to .github/workflows/trigger_release.yml index e332488c3..e800653e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/trigger_release.yml @@ -5,38 +5,40 @@ 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 - secrets: inherit + build_type: Release + is_qt_cached: false + secrets: + SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} + WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} + WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} + APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} create_release: needs: build_release - permissions: - contents: write - runs-on: ubuntu-slim + runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") @@ -45,13 +47,11 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-Linux-aarch64-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-*.AppImage/PrismLauncher-*-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage - mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync - 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-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz + mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage + mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync + mv PrismLauncher-macOS-Legacy*/PrismLauncher.zip PrismLauncher-macOS-Legacy-${{ env.VERSION }}.zip mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip - mv PrismLauncher-macOS*/PrismLauncher.dmg PrismLauncher-macOS-${{ env.VERSION }}.dmg tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -81,40 +81,23 @@ jobs: cd .. done - for d in PrismLauncher-Windows-MinGW-arm64*; do - cd "${d}" || continue - INST="$(echo -n ${d} | grep -o Setup || true)" - PORT="$(echo -n ${d} | grep -o Portable || true)" - NAME="PrismLauncher-Windows-MinGW-arm64" - test -z "${PORT}" || NAME="${NAME}-Portable" - test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe - test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * - cd .. - done - - name: Create release id: create_release - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@v2 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} name: Prism Launcher ${{ env.VERSION }} draft: true prerelease: false - fail_on_unmatched_files: true files: | + PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync - PrismLauncher-Linux-aarch64.AppImage - PrismLauncher-Linux-aarch64.AppImage.zsync PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe - PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip - PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip - PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe @@ -122,5 +105,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-macOS-Legacy-${{ env.VERSION }}.zip PrismLauncher-${{ env.VERSION }}.tar.gz diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index fa3e3b4d3..5e978f356 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -6,30 +6,25 @@ on: - cron: "0 0 * * 0" workflow_dispatch: -permissions: {} +permissions: + contents: write + pull-requests: write jobs: update-flake: if: github.repository == 'PrismLauncher/PrismLauncher' - - permissions: - contents: write - pull-requests: write - - runs-on: ubuntu-slim + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 - - uses: DeterminateSystems/update-flake-lock@v28 + - uses: DeterminateSystems/update-flake-lock@v24 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" pr-labels: | - platform: Linux - area: packaging - complexity: low - priority: low - type: robot + Linux + packaging + simple change changelog:omit diff --git a/.github/workflows/publish.yml b/.github/workflows/winget.yml similarity index 55% rename from .github/workflows/publish.yml rename to .github/workflows/winget.yml index 1bb1c5b50..eacf23099 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/winget.yml @@ -1,23 +1,13 @@ -name: Publish - +name: Publish to WinGet on: release: - types: [ released ] - -permissions: {} + types: [released] jobs: - winget: - name: Winget - - permissions: - contents: read - - runs-on: ubuntu-slim - + publish: + runs-on: windows-latest steps: - - name: Publish on Winget - uses: vedantmgoyal2009/winget-releaser@v2 + - uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} diff --git a/.gitignore b/.gitignore index 00afabbfa..b5523f685 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt -CMakeUserPresets.json /.project /.settings /.idea @@ -22,7 +21,6 @@ CMakeUserPresets.json /.vs cmake-build-*/ Debug -compile_commands.json # Build dirs build @@ -49,12 +47,8 @@ run/ # Nix/NixOS .direnv/ -## Used when manually invoking stdenv phases -outputs/ -## Regular artifacts +.pre-commit-config.yaml result -result-* -repl-result-* # Flatpak .flatpak-builder diff --git a/.gitmodules b/.gitmodules index 42c566fa8..0f437d277 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,24 @@ +[submodule "libraries/quazip"] + path = libraries/quazip + url = https://github.com/stachenov/quazip.git +[submodule "libraries/tomlplusplus"] + path = libraries/tomlplusplus + url = https://github.com/marzer/tomlplusplus.git +[submodule "libraries/filesystem"] + path = libraries/filesystem + url = https://github.com/gulrak/filesystem [submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git +[submodule "libraries/zlib"] + path = libraries/zlib + url = https://github.com/madler/zlib.git +[submodule "libraries/extra-cmake-modules"] + path = libraries/extra-cmake-modules + url = https://github.com/KDE/extra-cmake-modules +[submodule "libraries/cmark"] + path = libraries/cmark + url = https://github.com/commonmark/cmark.git +[submodule "flatpak/shared-modules"] + path = flatpak/shared-modules + url = https://github.com/flathub/shared-modules.git diff --git a/.markdownlintignore b/.markdownlintignore index 96f627ad9..a8669d01d 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1 +1,2 @@ libraries/nbtplusplus +libraries/quazip diff --git a/CMakeLists.txt b/CMakeLists.txt index 12afdefcc..4e7f14ac6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,6 @@ -cmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` +cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip -project(Launcher LANGUAGES C CXX) -if(APPLE) - enable_language(OBJC OBJCXX) -endif() +project(Launcher) string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) @@ -13,10 +10,6 @@ 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/") @@ -31,106 +24,101 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 17) 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}") -add_compile_definitions($<$>:QT_NO_DEBUG>) -add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x060400) -add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) + # /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}") -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>" - ) + # 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}") # /GL enables whole program optimizations - # 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() + # /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() # See https://github.com/ccache/ccache/issues/1040 - # TODO(@getchoo): Is sccache affected by this? Would be nice to use `ProgramDatabase`.... - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") + # 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() if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") endif() else() - 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() + set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") # ATL's pack list needs more than the default 1 Mib stack on windows - 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 - add_compile_options("$<$,$>:-ffunction-sections;-fdata-sections;-mguard=cf>") + if(WIN32) + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") endif() endif() -# 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() +# Fix build with Qt 5.13 +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") -option(USE_CLANG_TIDY "Enable the use of clang-tidy during compilation" OFF) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") -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() +# Fix aarch64 build for toml++ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") + +# set CXXFLAGS for build targets +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser -if (DEBUG_ADDRESS_SANITIZER) +if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND DEBUG_ADDRESS_SANITIZER) message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") - - set(USE_ASAN_COMPILE_OPTIONS $,$>) - if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") - message(STATUS "Using Address Sanitizer compile options for MSVC frontend") - add_compile_options( - $<${USE_ASAN_COMPILE_OPTIONS}:/fsanitize=address> - $<${USE_ASAN_COMPILE_OPTIONS}:/Oy-> - ) - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - message(STATUS "Using Address Sanitizer compile options for GCC/Clang") - add_compile_options( - $<${USE_ASAN_COMPILE_OPTIONS}:-fsanitize=address,undefined> - $<${USE_ASAN_COMPILE_OPTIONS}:-fno-omit-frame-pointer> - $<${USE_ASAN_COMPILE_OPTIONS}:-fno-sanitize-recover=null> - ) - link_libraries("asan" "ubsan") + if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + # using clang with clang-cl front end + message(STATUS "Address Sanitizer available on Clang MSVC frontend") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") + else() + # AppleClang and Clang + message(STATUS "Address Sanitizer available on Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + endif() + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + # GCC + message(STATUS "Address Sanitizer available on GCC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + link_libraries("asan") + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") + message(STATUS "Address Sanitizer available on MSVC") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() @@ -146,9 +134,8 @@ 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(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") message(STATUS "IPO / LTO enabled") else() message(STATUS "Not enabling IPO / LTO on debug builds") @@ -163,9 +150,20 @@ endif() option(BUILD_TESTING "Build the testing tree." ON) -find_package(ECM NO_MODULE REQUIRED) -set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") - +find_package(ECM QUIET NO_MODULE) +if(NOT ECM_FOUND) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/CMakeLists.txt") + message(STATUS "Using bundled ECM") + set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/modules;${CMAKE_MODULE_PATH}") + else() + message(FATAL_ERROR + " Could not find ECM\n \n" + " Either install ECM using the system package manager or clone submodules\n" + " Submodules can be cloned with 'git submodule update --init --recursive'") + endif() +else() + set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() include(CTest) include(ECMAddTests) if(BUILD_TESTING) @@ -177,19 +175,17 @@ 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_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_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") -set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") +set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 12) -set(Launcher_VERSION_MINOR 0) -set(Launcher_VERSION_PATCH 0) +set(Launcher_VERSION_MAJOR 9) +set(Launcher_VERSION_MINOR 2) -set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") -set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") -set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") +set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") +set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") +set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") @@ -223,10 +219,9 @@ set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL f set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.") # Builds +set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") -option(Launcher_USE_PCH "Use precompiled headers where possible" ON) - # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) @@ -234,7 +229,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) - set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader @@ -289,59 +284,75 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") ################################ 3rd Party Libs ################################ +# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values +# Record when fallback triggered and skip this find_package +if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB) + find_package(ZLIB QUIET) +endif() +if(NOT ZLIB_FOUND) + set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "") + mark_as_advanced(FORCE_BUNDLED_ZLIB) +endif() + # Find the required Qt parts -if(Launcher_QT_VERSION_MAJOR EQUAL 6) +include(QtVersionlessBackport) +if(Launcher_QT_VERSION_MAJOR EQUAL 5) + set(QT_VERSION_MAJOR 5) + find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth) + + if(NOT Launcher_FORCE_BUNDLED_LIBS) + find_package(QuaZip-Qt5 1.3 QUIET) + endif() + if (NOT QuaZip-Qt5_FOUND) + set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) + set(FORCE_BUNDLED_QUAZIP 1) + endif() + + # Qt 6 sets these by default. Notably causes Windows APIs to use UNICODE strings. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") +elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - 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) + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth) + list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) + + if(NOT Launcher_FORCE_BUNDLED_LIBS) + find_package(QuaZip-Qt6 1.3 QUIET) + endif() + if (NOT QuaZip-Qt6_FOUND) + set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) + set(FORCE_BUNDLED_QUAZIP 1) + endif() else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() -if(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 5) + include(ECMQueryQt) + ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) + ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) + ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) +else() set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() -find_package(cmark REQUIRED) - -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - find_package(PkgConfig REQUIRED) - pkg_check_modules(gamemode REQUIRED IMPORTED_TARGET gamemode) +# NOTE: Qt 6 already sets this by default +if (Qt5_POSITION_INDEPENDENT_CODE) + SET(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() -# Find libqrencode -## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll -if(MSVC) - find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED) - find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED) - find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded) - set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG}) -else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode) +if(NOT Launcher_FORCE_BUNDLED_LIBS) + # Find toml++ + find_package(tomlplusplus 3.2.0 QUIET) + + # Find ghc_filesystem + find_package(ghc_filesystem QUIET) + + # Find cmark + find_package(cmark QUIET) endif() -# Find libarchive through CMake packages, mainly for vcpkg -find_package(LibArchive) -# CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config -if(NOT LibArchive_FOUND) - find_package(PkgConfig REQUIRED) - pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) -endif() - -find_package(tomlplusplus 3.2.0) -# fallback to pkgconfig, important especially as many distros package toml++ built with meson -if(NOT tomlplusplus_FOUND) - find_package(PkgConfig REQUIRED) - pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus>=3.2.0) -endif() - -find_package(ZLIB REQUIRED) - - include(ECMQtDeclareLoggingCategory) ####################################### Program Info ####################################### @@ -355,7 +366,7 @@ set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) - set(Launcher_BUILD_UPDATER YES) + set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) @@ -371,10 +382,13 @@ if(UNIX AND APPLE) set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") + # Apps to bundle + set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app") + # 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 "${Launcher_AppID}") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.prismlauncher.${Launcher_Name}") 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}") @@ -383,63 +397,23 @@ if(UNIX AND APPLE) set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") - set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive") - set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + # directories to look for dependencies + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${MACOSX_SPARKLE_DIR}) + if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") set(Launcher_ENABLE_UPDATER YES) endif() + # install as bundle + set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") + # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) - find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") - if(ACTOOL_EXE) - execute_process( - COMMAND xcodebuild -version - OUTPUT_VARIABLE XCODE_VERSION_OUTPUT - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - string(REGEX MATCH "Xcode ([0-9]+\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") - if(XCODE_VERSION_MATCH) - set(XCODE_VERSION ${CMAKE_MATCH_1}) - else() - set(XCODE_VERSION 0.0) - endif() - - if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) - set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") - set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") - set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") - - add_custom_command( - OUTPUT "${GENERATED_ASSETS_CAR}" - COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" - --compile "${ASSETS_OUT_DIR}" - --output-partial-info-plist /dev/null - --app-icon ${Launcher_Name} - --enable-on-demand-resources NO - --target-device mac - --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} - --platform macosx - DEPENDS "${ICON_SOURCE}" - COMMENT "Compiling asset catalog (${ICON_SOURCE})" - VERBATIM - ) - add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") - install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") - else() - message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") - endif() - - else() - message(WARNING "actool not found. Cannot compile macOS app icons.\n" - "Install Xcode command line tools: 'xcode-select --install'") - endif() - - elseif(UNIX) include(KDEInstallDirs) @@ -447,20 +421,30 @@ elseif(UNIX) set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(JARS_DEST_DIR "share/${Launcher_Name}") + # install as bundle with no dependencies included + set(INSTALL_BUNDLE "nodeps" CACHE STRING "Use fixup_bundle to bundle dependencies") + # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR}) 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_BINARY_DIR}/${Launcher_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") - set(PLUGIN_DEST_DIR "plugins") - set(BUNDLE_DEST_DIR ".") - set(RESOURCES_DEST_DIR ".") + if (INSTALL_BUNDLE STREQUAL full) + set(PLUGIN_DEST_DIR "plugins") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + + # Apps to bundle + set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}") + + # directories to look for dependencies + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + endif() if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") @@ -476,6 +460,15 @@ elseif(WIN32) set(PLUGIN_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") + + # Apps to bundle + set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.exe") + + # directories to look for dependencies + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # install as bundle + set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") else() message(FATAL_ERROR "Platform not supported") endif() @@ -492,12 +485,70 @@ 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 +if(FORCE_BUNDLED_ZLIB) + message(STATUS "Using bundled zlib") + set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib + set(SKIP_INSTALL_ALL ON) + add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) + + # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. + # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. + check_include_file(unistd.h NEED_GENERATED_ZCONF) + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) + # zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162 + message(STATUS "Undoing Rename") + message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") + file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") + endif() + + set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE) + set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") + add_library(ZLIB::ZLIB ALIAS zlibstatic) + set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") + + find_package(ZLIB REQUIRED) +else() + message(STATUS "Using system zlib") +endif() +if (FORCE_BUNDLED_QUAZIP) + message(STATUS "Using bundled QuaZip") + set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts. + set(QUAZIP_INSTALL 0) + add_subdirectory(libraries/quazip) # zip manipulation library +else() + message(STATUS "Using system QuaZip") +endif() add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions +if(NOT tomlplusplus_FOUND) + message(STATUS "Using bundled tomlplusplus") + add_subdirectory(libraries/tomlplusplus) # toml parser +else() + message(STATUS "Using system tomlplusplus") +endif() +if(NOT cmark_FOUND) + message(STATUS "Using bundled cmark") + set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING}) + set(BUILD_TESTING 0) + set(BUILD_SHARED_LIBS 0) + add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser + add_library(cmark::cmark ALIAS cmark) + set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING}) +else() + message(STATUS "Using system cmark") +endif() +add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API +if (NOT ghc_filesystem_FOUND) + message(STATUS "Using bundled ghc_filesystem") + add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS +else() + message(STATUS "Using system ghc_filesystem") +endif() add_subdirectory(libraries/qdcss) # css parser ############################### Built Artifacts ############################### diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index f8496acb6..000000000 --- a/CMakePresets.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "$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 f4b12d08b..072916772 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,113 +1,17 @@ # Contributions Guidelines -## Restrictions on Generative AI Usage (AI Policy) +## Code formatting -> [!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) +Try to follow the existing formatting. +If there is no existing formatting, you may use `clang-format` with our included `.clang-format` configuration. -We expect authentic engagement in our community. +In general, in order of importance: -- 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! - -Please also follow the project's conventions for C++: - -- Class and type names should be formatted as `PascalCase`: `MyClass`. -- Private or protected class data members should be formatted as `camelCase` prefixed with `m_`: `m_myCounter`. -- Private or protected `static` class data members should be formatted as `camelCase` prefixed with `s_`: `s_instance`. -- Public class data members should be formatted as `camelCase` without the prefix: `dateOfBirth`. -- Public, private or protected `static const` class data members should be formatted as `SCREAMING_SNAKE_CASE`: `MAX_VALUE`. -- Class function members should be formatted as `camelCase` without a prefix: `incrementCounter`. -- Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`. -- `const` global variables 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. - - A function has side effects and an error status is returned. - - A function is likely be mistaken for having side effects. -- A plain getter is unlikely to cause confusion and adding `[[nodiscard]]` can create clutter and inconsistency. - -Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations. - -Here is what these conventions with the formatting configuration look like: - -```c++ -#define AWESOMENESS 10 - -constexpr double PI = 3.14159; - -enum class PizzaToppings { HamAndPineapple, OreoAndKetchup }; - -struct Person { - QString name; - QDateTime dateOfBirth; - - long daysOld() const { return dateOfBirth.daysTo(QDateTime::currentDateTime()); } -}; - -class ImportantClass { - public: - void incrementCounter() - { - if (m_counter + 1 > MAX_COUNTER_VALUE) - throw std::runtime_error("Counter has reached limit!"); - - ++m_counter; - } - - int counter() const { return m_counter; } - - private: - static constexpr int MAX_COUNTER_VALUE = 100; - int m_counter; -}; - -ImportantClass importantClassInstance; -``` - -If you see any names which do not follow these conventions, it is preferred that you leave them be - renames increase the number of changes therefore make reviewing harder and make your PR more prone to conflicts. However, if you're refactoring a whole class anyway, it's fine. +- Make sure your IDE is not messing up line endings or whitespace and avoid using linters. +- Prefer readability over dogma. +- Keep to the existing formatting. +- Indent with 4 space unless it's in a submodule. +- Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. ## Signing your work diff --git a/COPYING.md b/COPYING.md index 52f29f2e6..111587060 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## Prism Launcher Prism Launcher - Minecraft Launcher - Copyright (C) 2022-2026 Prism Launcher Contributors + Copyright (C) 2022-2024 Prism Launcher Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -108,7 +108,7 @@ Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. -## Qt 6 +## Qt 5/6 Copyright (C) 2022 The Qt Company Ltd and other contributors. Contact: https://www.qt.io/licensing @@ -212,6 +212,30 @@ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL +## Quazip + + Copyright (C) 2005-2021 Sergey A. Tachenov + + The QuaZip library is licensed under the GNU Lesser General Public + License V2.1 plus a static linking exception. + + The original ZIP/UNZIP package (MiniZip) is copyrighted by Gilles + Vollant and contributors, see quazip/(un)zip.h files for details. + Basically it's the zlib license. + + STATIC LINKING EXCEPTION + + The copyright holders give you permission to link this library with + independent modules to produce an executable, regardless of the license + terms of these independent modules, and to copy and distribute the + resulting executable under terms of your choice, provided that you also + meet, for each linked independent module, the terms and conditions of + the license of that module. An independent module is a module which is + not derived from or based on this library. If you modify this library, + you must extend this exception to your version of the library. + + See COPYING file for the full LGPL text. + ## launcher (`libraries/launcher`) PolyMC - Minecraft Launcher @@ -338,6 +362,28 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## gulrak/filesystem + + Copyright (c) 2018, Steffen Schümann + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ## Breeze icons Copyright (C) 2014 Uri Herrera and others @@ -379,44 +425,3 @@ You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . - -## libqrencode (`fukuchi/libqrencode`) - - Copyright (C) 2020 libqrencode Authors - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -## vcpkg (`cmake/vcpkg-ports`) - - MIT License - - Copyright (c) Microsoft Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this - software and associated documentation files (the "Software"), to deal in the Software - without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be included in all copies - or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Containerfile b/Containerfile deleted file mode 100644 index 59595fe55..000000000 --- a/Containerfile +++ /dev/null @@ -1,74 +0,0 @@ -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 ac6cfd96b..9c4909509 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,18 @@ 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://prismlauncher.org/nightly) (this will always point only to the latest version of develop) +- [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contribuitors) +- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/trigger_builds/develop) (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**. -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). +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. ## Community & Support @@ -57,7 +61,12 @@ The translation effort for Prism Launcher is hosted on [Weblate](https://hosted. ## Building -If you want to build Prism Launcher yourself, check the [build instructions](https://prismlauncher.org/wiki/development/build-instructions). +If you want to build Prism Launcher yourself, check the build instructions: + +- [Windows](https://prismlauncher.org/wiki/development/build-instructions/windows/) +- [Linux](https://prismlauncher.org/wiki/development/build-instructions/linux/) +- [MacOS](https://prismlauncher.org/wiki/development/build-instructions/macos/) +- [OpenBSD](https://prismlauncher.org/wiki/development/build-instructions/openbsd/) ## Sponsors & Partners @@ -67,13 +76,7 @@ We thank all the wonderful backers over at Open Collective! Support Prism Launch Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). - - - - - JetBrains logo - - +[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/opensource/) Thanks to Weblate for hosting our translation efforts. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 14d8236d8..b48232b43 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -33,8 +33,9 @@ * limitations under the License. */ -#include +#include #include "BuildConfig.h" +#include const Config BuildConfig; @@ -48,16 +49,15 @@ Config::Config() LAUNCHER_DOMAIN = "@Launcher_Domain@"; LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; LAUNCHER_GIT = "@Launcher_Git@"; - LAUNCHER_APPID = "@Launcher_AppID@"; + LAUNCHER_DESKTOPFILENAME = "@Launcher_DesktopFileName@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; - LAUNCHER_ENVNAME = "@Launcher_ENVName@"; USER_AGENT = "@Launcher_UserAgent@"; + USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; - VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; @@ -74,13 +74,14 @@ Config::Config() MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; - if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) + { UPDATER_ENABLED = true; - } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } -#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER + #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; @@ -88,32 +89,39 @@ Config::Config() GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" - if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || - GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") + || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") + || GIT_REFSPEC == QStringLiteral("") + || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) + { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } - if (GIT_REFSPEC.startsWith("refs/heads/")) { + if (GIT_REFSPEC.startsWith("refs/heads/")) + { VERSION_CHANNEL = GIT_REFSPEC; - VERSION_CHANNEL.remove("refs/heads/"); - } else if (!GIT_COMMIT.isEmpty()) { + VERSION_CHANNEL.remove("refs/heads/"); + } + else if (!GIT_COMMIT.isEmpty()) + { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); - } else { + } + else + { VERSION_CHANNEL = "unknown"; } 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@"; - LEGACY_FMLLIBS_BASE_URL = "@Launcher_LEGACY_FMLLIBS_BASE_URL@"; + FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; @@ -128,7 +136,7 @@ Config::Config() QString Config::versionString() const { - return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); + return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR); } QString Config::printableVersionString() const @@ -136,7 +144,8 @@ QString Config::printableVersionString() const QString vstr = versionString(); // If the build is not a main release, append the channel - if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { + if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr) + { vstr += "-" + VERSION_CHANNEL; } return vstr; @@ -153,3 +162,4 @@ QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a5851bfba..ae705d098 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -52,16 +52,13 @@ class Config { QString LAUNCHER_DOMAIN; QString LAUNCHER_CONFIGFILE; QString LAUNCHER_GIT; - QString LAUNCHER_APPID; + QString LAUNCHER_DESKTOPFILENAME; QString LAUNCHER_SVGFILENAME; - QString LAUNCHER_ENVNAME; /// The major version number. int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; - /// The patch version number. - int VERSION_PATCH; /** * The version channel @@ -108,6 +105,9 @@ class Config { /// User-Agent to use. QString USER_AGENT; + /// User-Agent to use for uncached requests. + QString USER_AGENT_UNCACHED; + /// The git commit hash of this build QString GIT_COMMIT; @@ -129,12 +129,7 @@ class Config { QString NEWS_OPEN_URL; /** - * 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 + * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help */ QString HELP_URL; @@ -172,13 +167,13 @@ class Config { QString DISCORD_URL; QString SUBREDDIT_URL; - QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; + QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString LEGACY_FMLLIBS_BASE_URL; + QString FMLLIBS_BASE_URL; QString TRANSLATION_FILES_URL; - QString FTB_API_BASE_URL = "https://api.feed-the-beast.com/v1/modpacks/public"; + QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; @@ -194,10 +189,8 @@ 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 new file mode 100644 index 000000000..51d2fb13a --- /dev/null +++ b/cmake/CompilerWarnings.cmake @@ -0,0 +1,163 @@ +# +# 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/ECMQueryQt.cmake b/cmake/ECMQueryQt.cmake new file mode 100644 index 000000000..98eb50089 --- /dev/null +++ b/cmake/ECMQueryQt.cmake @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2014 Rohan Garg +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014-2016 Aleix Pol +# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau +# SPDX-FileCopyrightText: 2022 Ahmad Samir +# +# SPDX-License-Identifier: BSD-3-Clause +#[=======================================================================[.rst: +ECMQueryQt +--------------- +This module can be used to query the installation paths used by Qt. + +For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in +support to query the paths of a target platform when cross-compiling). + +This module defines the following function: +:: + + ecm_query_qt( [TRY]) + +Passing ``TRY`` will result in the method not making the build fail if the executable +used for querying has not been found, but instead simply print a warning message and +return an empty string. + +Example usage: + +.. code-block:: cmake + + include(ECMQueryQt) + ecm_query_qt(bin_dir QT_INSTALL_BINS) + +If the call succeeds ``${bin_dir}`` will be set to ``/path/to/bin/dir`` (e.g. +``/usr/lib64/qt/bin/``). + +Since: 5.93 +#]=======================================================================] + +include(${CMAKE_CURRENT_LIST_DIR}/QtVersionOption.cmake) +include(CheckLanguage) +check_language(CXX) +if (CMAKE_CXX_COMPILER) + # Enable the CXX language to let CMake look for config files in library dirs. + # See: https://gitlab.kitware.com/cmake/cmake/-/issues/23266 + enable_language(CXX) +endif() + +if (QT_MAJOR_VERSION STREQUAL "5") + # QUIET to accommodate the TRY option + find_package(Qt${QT_MAJOR_VERSION}Core QUIET) + if(TARGET Qt5::qmake) + get_target_property(_qmake_executable_default Qt5::qmake LOCATION) + + set(QUERY_EXECUTABLE ${_qmake_executable_default} + CACHE FILEPATH "Location of the Qt5 qmake executable") + set(_exec_name_text "Qt5 qmake") + set(_cli_option "-query") + endif() +elseif(QT_MAJOR_VERSION STREQUAL "6") + # QUIET to accommodate the TRY option + find_package(Qt6 COMPONENTS CoreTools QUIET CONFIG) + if (TARGET Qt6::qtpaths) + get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION) + + set(QUERY_EXECUTABLE ${_qtpaths_executable} + CACHE FILEPATH "Location of the Qt6 qtpaths executable") + set(_exec_name_text "Qt6 qtpaths") + set(_cli_option "--query") + endif() +endif() + +function(ecm_query_qt result_variable qt_variable) + set(options TRY) + set(oneValueArgs) + set(multiValueArgs) + + cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT QUERY_EXECUTABLE) + if(ARGS_TRY) + set(${result_variable} "" PARENT_SCOPE) + message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}") + return() + else() + message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required") + endif() + endif() + execute_process( + COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}" + RESULT_VARIABLE return_code + OUTPUT_VARIABLE output + ) + if(return_code EQUAL 0) + string(STRIP "${output}" output) + file(TO_CMAKE_PATH "${output}" output_path) + set(${result_variable} "${output_path}" PARENT_SCOPE) + else() + message(WARNING "Failed call: ${_command} \"${qt_variable}\"") + message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}") + endif() +endfunction() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index eb40bacfd..3a8c8fbfe 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 - ${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. + Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears. NSLocalNetworkUsageDescription Minecraft uses the local network to find and connect to LAN servers. NSPrincipalClass @@ -21,9 +21,7 @@ CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile - ${Launcher_Name} - CFBundleIconName - ${Launcher_Name} + ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion @@ -44,8 +42,6 @@ LSRequiresCarbon - LSApplicationCategoryType - public.app-category.games NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} SUPublicEDKey @@ -61,7 +57,7 @@ mrpack CFBundleTypeName - ${Launcher_DisplayName} instance + Prism Launcher instance CFBundleTypeOSTypes TEXT @@ -87,11 +83,10 @@ CFBundleURLName - ${Launcher_Name} + Prismlauncher CFBundleURLSchemes prismlauncher - ${MACOSX_BUNDLE_EXECUTABLE_NAME} diff --git a/cmake/QtVersionOption.cmake b/cmake/QtVersionOption.cmake new file mode 100644 index 000000000..1390f9db6 --- /dev/null +++ b/cmake/QtVersionOption.cmake @@ -0,0 +1,38 @@ +#.rst: +# QtVersionOption +# --------------- +# +# Adds a build option to select the major Qt version if necessary, +# that is, if the major Qt version has not yet been determined otherwise +# (e.g. by a corresponding find_package() call). +# +# This module is typically included by other modules requiring knowledge +# about the major Qt version. +# +# ``QT_MAJOR_VERSION`` is defined to either be "5" or "6". +# +# +# Since 5.82.0. + +#============================================================================= +# SPDX-FileCopyrightText: 2021 Volker Krause +# +# SPDX-License-Identifier: BSD-3-Clause + +if (DEFINED QT_MAJOR_VERSION) + return() +endif() + +if (TARGET Qt5::Core) + set(QT_MAJOR_VERSION 5) +elseif (TARGET Qt6::Core) + set(QT_MAJOR_VERSION 6) +else() + option(BUILD_WITH_QT6 "Build against Qt 6" OFF) + + if (BUILD_WITH_QT6) + set(QT_MAJOR_VERSION 6) + else() + set(QT_MAJOR_VERSION 5) + endif() +endif() diff --git a/cmake/QtVersionlessBackport.cmake b/cmake/QtVersionlessBackport.cmake new file mode 100644 index 000000000..46792db58 --- /dev/null +++ b/cmake/QtVersionlessBackport.cmake @@ -0,0 +1,97 @@ +#============================================================================= +# Copyright 2005-2011 Kitware, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Kitware, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# From Qt5CoreMacros.cmake + +function(qt_generate_moc) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_generate_moc(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_generate_moc(${ARGV}) + endif() +endfunction() + +function(qt_wrap_cpp outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_wrap_cpp("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_wrap_cpp("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_add_binary_resources) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_binary_resources(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_binary_resources(${ARGV}) + endif() +endfunction() + +function(qt_add_resources outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_resources("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_resources("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_add_big_resources outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_add_big_resources(${outfiles} ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_add_big_resources(${outfiles} ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + +function(qt_import_plugins) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_import_plugins(${ARGV}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_import_plugins(${ARGV}) + endif() +endfunction() + + +# From Qt5WidgetsMacros.cmake + +function(qt_wrap_ui outfiles) + if(QT_VERSION_MAJOR EQUAL 5) + qt5_wrap_ui("${outfiles}" ${ARGN}) + elseif(QT_VERSION_MAJOR EQUAL 6) + qt6_wrap_ui("${outfiles}" ${ARGN}) + endif() + set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) +endfunction() + diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/README.md b/cmake/vcpkg-ports/vcpkg-tool-meson/README.md deleted file mode 100644 index 9047c8037..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The only difference between this and the upstream vcpkg port is the addition of `universal-osx.patch`. It's very annoying we need to bundle this entire tree to do that. - --@getchoo diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch deleted file mode 100644 index ad800aa66..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py -index 11a00be5d..89ae490ff 100644 ---- a/mesonbuild/cmake/toolchain.py -+++ b/mesonbuild/cmake/toolchain.py -@@ -202,7 +202,7 @@ class CMakeToolchain: - @staticmethod - def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: - if compiler.get_argument_syntax() == 'msvc': -- return arg.startswith('/') -+ return arg.startswith(('/','-')) - else: - if os.path.basename(compiler.get_exe()) == 'zig' and arg in {'ar', 'cc', 'c++', 'dlltool', 'lib', 'ranlib', 'objcopy', 'rc'}: - return True diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch deleted file mode 100644 index 0cbfe717d..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch +++ /dev/null @@ -1,45 +0,0 @@ -diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py -index 883a29a..d9a82af 100644 ---- a/mesonbuild/dependencies/python.py -+++ b/mesonbuild/dependencies/python.py -@@ -232,8 +232,10 @@ class _PythonDependencyBase(_Base): - else: - if self.is_freethreaded: - libpath = Path('libs') / f'python{vernum}t.lib' -+ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}t.lib' - else: - libpath = Path('libs') / f'python{vernum}.lib' -+ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}.lib' - # For a debug build, pyconfig.h may force linking with - # pythonX_d.lib (see meson#10776). This cannot be avoided - # and won't work unless we also have a debug build of -@@ -250,6 +252,8 @@ class _PythonDependencyBase(_Base): - vscrt = self.env.coredata.optstore.get_value('b_vscrt') - if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: - vscrt_debug = True -+ if is_debug_build: -+ libpath = Path('libs') / f'..' / f'..' / f'..' / f'debug/lib' / f'python{vernum}_d.lib' - if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): - mlog.warning(textwrap.dedent('''\ - Using a debug build type with MSVC or an MSVC-compatible compiler -@@ -350,9 +354,10 @@ class PythonSystemDependency(SystemDependency, _PythonDependencyBase): - self.is_found = True - - # compile args -+ verdot = self.variables.get('py_version_short') - inc_paths = mesonlib.OrderedSet([ - self.variables.get('INCLUDEPY'), -- self.paths.get('include'), -+ self.paths.get('include') + f'/../../../include/python${verdot}', - self.paths.get('platinclude')]) - - self.compile_args += ['-I' + path for path in inc_paths if path] -@@ -416,7 +421,7 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', - candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) - # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, - # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. -- if pkg_libdir is not None: -+ if True or pkg_libdir is not None: - candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) - else: - candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch deleted file mode 100644 index 394b064dc..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch +++ /dev/null @@ -1,52 +0,0 @@ -From a16ec8b0fb6d7035b669a13edd4d97ff0c307a0b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Martin=20D=C3=B8rum?= -Date: Fri, 2 May 2025 10:56:28 +0200 -Subject: [PATCH] cpp: fix _LIBCPP_ENABLE_ASSERTIONS warning - -libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS from version 18. -However, the libc++ shipped with Apple Clang backported that -deprecation in version 17 already, -which is the version which Apple currently ships for macOS. -This PR changes the _LIBCPP_ENABLE_ASSERTIONS deprecation check -to use version ">=17" on Apple Clang. ---- - mesonbuild/compilers/cpp.py | 12 ++++++++++-- - 1 file changed, 10 insertions(+), 2 deletions(-) - -diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py -index 01b9bb9fa34f..f7dc150e8608 100644 ---- a/mesonbuild/compilers/cpp.py -+++ b/mesonbuild/compilers/cpp.py -@@ -311,6 +311,9 @@ def get_option_link_args(self, target: 'BuildTarget', env: 'Environment', subpro - return libs - return [] - -+ def is_libcpp_enable_assertions_deprecated(self) -> bool: -+ return version_compare(self.version, ">=18") -+ - def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: - if disable: - return ['-DNDEBUG'] -@@ -323,7 +326,7 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: - if self.language_stdlib_provider(env) == 'stdc++': - return ['-D_GLIBCXX_ASSERTIONS=1'] - else: -- if version_compare(self.version, '>=18'): -+ if self.is_libcpp_enable_assertions_deprecated(): - return ['-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST'] - elif version_compare(self.version, '>=15'): - return ['-D_LIBCPP_ENABLE_ASSERTIONS=1'] -@@ -343,7 +346,12 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): - - - class AppleClangCPPCompiler(AppleCompilerMixin, AppleCPPStdsMixin, ClangCPPCompiler): -- pass -+ def is_libcpp_enable_assertions_deprecated(self) -> bool: -+ # Upstream libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS -+ # in favor of _LIBCPP_HARDENING_MODE from version 18 onwards, -+ # but Apple Clang 17's libc++ has back-ported that change. -+ # See: https://github.com/mesonbuild/meson/issues/14440 -+ return version_compare(self.version, ">=17") - - - class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake deleted file mode 100644 index 84201aa1a..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake +++ /dev/null @@ -1,5 +0,0 @@ -file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/meson") -file(INSTALL "${SOURCE_PATH}/meson.py" - "${SOURCE_PATH}/mesonbuild" - DESTINATION "${CURRENT_PACKAGES_DIR}/tools/meson" -) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch deleted file mode 100644 index 8f2a029de..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py ---- a/mesonbuild/dependencies/misc.py -+++ b/mesonbuild/dependencies/misc.py -@@ -593,7 +593,8 @@ iconv_factory = DependencyFactory( - - packages['intl'] = intl_factory = DependencyFactory( - 'intl', -+ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], -+ cmake_name='Intl', -- [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], - builtin_class=IntlBuiltinDependency, - system_class=IntlSystemDependency, - ) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in b/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in deleted file mode 100644 index df21b753b..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in +++ /dev/null @@ -1,43 +0,0 @@ -[binaries] -cmake = ['@CMAKE_COMMAND@'] -ninja = ['@NINJA@'] -pkg-config = ['@PKGCONFIG@'] -@MESON_MT@ -@MESON_AR@ -@MESON_RC@ -@MESON_C@ -@MESON_C_LD@ -@MESON_CXX@ -@MESON_CXX_LD@ -@MESON_OBJC@ -@MESON_OBJC_LD@ -@MESON_OBJCPP@ -@MESON_OBJCPP_LD@ -@MESON_FC@ -@MESON_FC_LD@ -@MESON_WINDRES@ -@MESON_ADDITIONAL_BINARIES@ -[properties] -cmake_toolchain_file = '@SCRIPTS@/buildsystems/vcpkg.cmake' -@MESON_ADDITIONAL_PROPERTIES@ -[cmake] -CMAKE_BUILD_TYPE = '@MESON_CMAKE_BUILD_TYPE@' -VCPKG_TARGET_TRIPLET = '@TARGET_TRIPLET@' -VCPKG_HOST_TRIPLET = '@_HOST_TRIPLET@' -VCPKG_CHAINLOAD_TOOLCHAIN_FILE = '@VCPKG_CHAINLOAD_TOOLCHAIN_FILE@' -VCPKG_CRT_LINKAGE = '@VCPKG_CRT_LINKAGE@' -_VCPKG_INSTALLED_DIR = '@_VCPKG_INSTALLED_DIR@' -@MESON_HOST_MACHINE@ -@MESON_BUILD_MACHINE@ -[built-in options] -default_library = '@MESON_DEFAULT_LIBRARY@' -werror = false -@MESON_CFLAGS@ -@MESON_CXXFLAGS@ -@MESON_FCFLAGS@ -@MESON_OBJCFLAGS@ -@MESON_OBJCPPFLAGS@ -# b_vscrt -@MESON_VSCRT_LINKAGE@ -# c_winlibs/cpp_winlibs -@MESON_WINLIBS@ \ No newline at end of file diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake deleted file mode 100644 index fdea886a7..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake +++ /dev/null @@ -1,45 +0,0 @@ -# This port represents a dependency on the Meson build system. -# In the future, it is expected that this port acquires and installs Meson. -# Currently is used in ports that call vcpkg_find_acquire_program(MESON) in order to force rebuilds. - -set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled) - -set(patches - meson-intl.patch - adjust-python-dep.patch - adjust-args.patch - remove-freebsd-pcfile-specialization.patch - fix-libcpp-enable-assertions.patch # https://github.com/mesonbuild/meson/pull/14548, Remove in 1.8.3 - universal-osx.patch # NOTE(@getchoo): THIS IS THE ONLY CHANGE NEEDED FOR PRISM -) -set(scripts - vcpkg-port-config.cmake - vcpkg_configure_meson.cmake - vcpkg_install_meson.cmake - meson.template.in -) -set(to_hash - "${CMAKE_CURRENT_LIST_DIR}/vcpkg.json" - "${CMAKE_CURRENT_LIST_DIR}/portfile.cmake" -) -foreach(file IN LISTS patches scripts) - set(filepath "${CMAKE_CURRENT_LIST_DIR}/${file}") - list(APPEND to_hash "${filepath}") - file(COPY "${filepath}" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") -endforeach() - -set(meson_path_hash "") -foreach(filepath IN LISTS to_hash) - file(SHA1 "${filepath}" to_append) - string(APPEND meson_path_hash "${to_append}") -endforeach() -string(SHA512 meson_path_hash "${meson_path_hash}") - -string(SUBSTRING "${meson_path_hash}" 0 6 MESON_SHORT_HASH) -list(TRANSFORM patches REPLACE [[^(..*)$]] [["${CMAKE_CURRENT_LIST_DIR}/\0"]]) -list(JOIN patches "\n " PATCHES) -configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY) - -vcpkg_install_copyright(FILE_LIST "${VCPKG_ROOT_DIR}/LICENSE.txt") - -include("${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch deleted file mode 100644 index 947345ccf..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py -index cc0450a52..13501466d 100644 ---- a/mesonbuild/modules/pkgconfig.py -+++ b/mesonbuild/modules/pkgconfig.py -@@ -701,16 +701,8 @@ class PkgConfigModule(NewExtensionModule): - pcfile = filebase + '.pc' - pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir - if pkgroot is None: -- m = state.environment.machines.host -- if m.is_freebsd(): -- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'libdata', 'pkgconfig') -- pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') -- elif m.is_haiku(): -- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') -- pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') -- else: -- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') -- pkgroot_name = os.path.join('{libdir}', 'pkgconfig') -+ pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') -+ pkgroot_name = os.path.join('{libdir}', 'pkgconfig') - relocatable = state.get_option('pkgconfig.relocatable') - self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, - version, pcfile, conflicts, variables, diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch deleted file mode 100644 index 58b96d5ce..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch +++ /dev/null @@ -1,16 +0,0 @@ -diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py -index f57957f0b..a72e72a0b 100644 ---- a/mesonbuild/compilers/detect.py -+++ b/mesonbuild/compilers/detect.py -@@ -1472,6 +1472,11 @@ def _get_clang_compiler_defines(compiler: T.List[str], lang: str) -> T.Dict[str, - """ - from .mixins.clang import clang_lang_map - -+ # Filter out `-arch` flags passed to the compiler for Universal Binaries -+ # https://github.com/mesonbuild/meson/issues/5290 -+ # https://github.com/mesonbuild/meson/issues/8206 -+ compiler = [arg for i, arg in enumerate(compiler) if not (i > 0 and compiler[i - 1] == "-arch") and not arg == "-arch"] -+ - def _try_obtain_compiler_defines(args: T.List[str]) -> str: - mlog.debug(f'Running command: {join_args(args)}') - p, output, error = Popen_safe(compiler + args, write='', stdin=subprocess.PIPE) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake deleted file mode 100644 index c0dee3a38..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake +++ /dev/null @@ -1,62 +0,0 @@ -include("${CURRENT_HOST_INSTALLED_DIR}/share/vcpkg-cmake-get-vars/vcpkg-port-config.cmake") -# Overwrite builtin scripts -include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_configure_meson.cmake") -include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_install_meson.cmake") - -set(meson_short_hash @MESON_SHORT_HASH@) - -# Setup meson: -set(program MESON) -set(program_version @VERSION@) -set(program_name meson) -set(search_names meson meson.py) -set(ref "${program_version}") -set(path_to_search "${DOWNLOADS}/tools/meson-${program_version}-${meson_short_hash}") -set(download_urls "https://github.com/mesonbuild/meson/archive/${ref}.tar.gz") -set(download_filename "meson-${ref}.tar.gz") -set(download_sha512 bd2e65f0863d9cb974e659ff502d773e937b8a60aaddfd7d81e34cd2c296c8e82bf214d790ac089ba441543059dfc2677ba95ed51f676df9da420859f404a907) - -find_program(SCRIPT_MESON NAMES ${search_names} PATHS "${path_to_search}" NO_DEFAULT_PATH) # NO_DEFAULT_PATH due top patching - -if(NOT SCRIPT_MESON) - vcpkg_download_distfile(archive_path - URLS ${download_urls} - SHA512 "${download_sha512}" - FILENAME "${download_filename}" - ) - file(REMOVE_RECURSE "${path_to_search}") - file(REMOVE_RECURSE "${path_to_search}-tmp") - file(MAKE_DIRECTORY "${path_to_search}-tmp") - file(ARCHIVE_EXTRACT INPUT "${archive_path}" - DESTINATION "${path_to_search}-tmp" - #PATTERNS "**/mesonbuild/*" "**/*.py" - ) - z_vcpkg_apply_patches( - SOURCE_PATH "${path_to_search}-tmp/meson-${ref}" - PATCHES - @PATCHES@ - ) - file(MAKE_DIRECTORY "${path_to_search}") - file(RENAME "${path_to_search}-tmp/meson-${ref}/meson.py" "${path_to_search}/meson.py") - file(RENAME "${path_to_search}-tmp/meson-${ref}/mesonbuild" "${path_to_search}/mesonbuild") - file(REMOVE_RECURSE "${path_to_search}-tmp") - set(SCRIPT_MESON "${path_to_search}/meson.py") -endif() - -# Check required python version -vcpkg_find_acquire_program(PYTHON3) -vcpkg_execute_in_download_mode( - COMMAND "${PYTHON3}" --version - OUTPUT_VARIABLE version_contents - WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" -) -string(REGEX MATCH [[[0-9]+\.[0-9]+\.[0-9]+]] python_ver "${version_contents}") - -set(min_required 3.7) -if(python_ver VERSION_LESS "${min_required}") - message(FATAL_ERROR "Found Python version '${python_ver} at ${PYTHON3}' is insufficient for meson. meson requires at least version '${min_required}'") -else() - message(STATUS "Found Python version '${python_ver} at ${PYTHON3}'") -endif() - -message(STATUS "Using meson: ${SCRIPT_MESON}") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json deleted file mode 100644 index 04a0cbbec..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "vcpkg-tool-meson", - "version": "1.8.2", - "description": "Meson build system", - "homepage": "https://github.com/mesonbuild/meson", - "license": "Apache-2.0", - "supports": "native", - "dependencies": [ - "vcpkg-cmake-get-vars" - ] -} diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake deleted file mode 100644 index 6b00200d1..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake +++ /dev/null @@ -1,480 +0,0 @@ -function(z_vcpkg_meson_set_proglist_variables config_type) - if(VCPKG_TARGET_IS_WINDOWS) - set(proglist MT AR) - else() - set(proglist AR RANLIB STRIP NM OBJDUMP DLLTOOL MT) - endif() - foreach(prog IN LISTS proglist) - if(VCPKG_DETECTED_CMAKE_${prog}) - if(meson_${prog}) - string(TOUPPER "MESON_${meson_${prog}}" var_to_set) - set("${var_to_set}" "${meson_${prog}} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) - elseif(${prog} STREQUAL AR AND VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}) - # Probably need to move AR somewhere else - string(TOLOWER "${prog}" proglower) - z_vcpkg_meson_convert_compiler_flags_to_list(ar_flags "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}}") - list(PREPEND ar_flags "${VCPKG_DETECTED_CMAKE_${prog}}") - z_vcpkg_meson_convert_list_to_python_array(ar_flags ${ar_flags}) - set("MESON_AR" "${proglower} = ${ar_flags}" PARENT_SCOPE) - else() - string(TOUPPER "MESON_${prog}" var_to_set) - string(TOLOWER "${prog}" proglower) - set("${var_to_set}" "${proglower} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) - endif() - endif() - endforeach() - set(compilers "${arg_LANGUAGES}") - if(VCPKG_TARGET_IS_WINDOWS) - list(APPEND compilers RC) - endif() - set(meson_RC windres) - set(meson_Fortran fortran) - set(meson_CXX cpp) - foreach(prog IN LISTS compilers) - if(VCPKG_DETECTED_CMAKE_${prog}_COMPILER) - string(TOUPPER "MESON_${prog}" var_to_set) - if(meson_${prog}) - if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) - # Need compiler flags in prog vars for sanity check. - z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") - endif() - list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") - list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise - z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) - set("${var_to_set}" "${meson_${prog}} = ${${prog}flags}" PARENT_SCOPE) - if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID - AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" - AND VCPKG_DETECTED_CMAKE_LINKER) - string(TOUPPER "MESON_${prog}_LD" var_to_set) - set(${var_to_set} "${meson_${prog}}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) - endif() - else() - if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) - # Need compiler flags in prog vars for sanity check. - z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") - endif() - list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") - list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise - z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) - string(TOLOWER "${prog}" proglower) - set("${var_to_set}" "${proglower} = ${${prog}flags}" PARENT_SCOPE) - if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID - AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" - AND VCPKG_DETECTED_CMAKE_LINKER) - string(TOUPPER "MESON_${prog}_LD" var_to_set) - set(${var_to_set} "${proglower}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) - endif() - endif() - endif() - endforeach() -endfunction() - -function(z_vcpkg_meson_convert_compiler_flags_to_list out_var compiler_flags) - separate_arguments(cmake_list NATIVE_COMMAND "${compiler_flags}") - list(TRANSFORM cmake_list REPLACE ";" [[\\;]]) - set("${out_var}" "${cmake_list}" PARENT_SCOPE) -endfunction() - -function(z_vcpkg_meson_convert_list_to_python_array out_var) - z_vcpkg_function_arguments(flag_list 1) - vcpkg_list(REMOVE_ITEM flag_list "") # remove empty elements if any - vcpkg_list(JOIN flag_list "', '" flag_list) - set("${out_var}" "['${flag_list}']" PARENT_SCOPE) -endfunction() - -# Generates the required compiler properties for meson -function(z_vcpkg_meson_set_flags_variables config_type) - if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) - set(libpath_flag /LIBPATH:) - else() - set(libpath_flag -L) - endif() - if(config_type STREQUAL "DEBUG") - set(path_suffix "/debug") - else() - set(path_suffix "") - endif() - - set(includepath "-I${CURRENT_INSTALLED_DIR}/include") - set(libpath "${libpath_flag}${CURRENT_INSTALLED_DIR}${path_suffix}/lib") - - foreach(lang IN LISTS arg_LANGUAGES) - z_vcpkg_meson_convert_compiler_flags_to_list(${lang}flags "${VCPKG_COMBINED_${lang}_FLAGS_${config_type}}") - if(lang MATCHES "^(C|CXX)$") - vcpkg_list(APPEND ${lang}flags "${includepath}") - endif() - z_vcpkg_meson_convert_list_to_python_array(${lang}flags ${${lang}flags}) - set(lang_mapping "${lang}") - if(lang STREQUAL "Fortran") - set(lang_mapping "FC") - endif() - string(TOLOWER "${lang_mapping}" langlower) - if(lang STREQUAL "CXX") - set(langlower cpp) - endif() - set(MESON_${lang_mapping}FLAGS "${langlower}_args = ${${lang}flags}\n") - set(linker_flags "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_${config_type}}") - z_vcpkg_meson_convert_compiler_flags_to_list(linker_flags "${linker_flags}") - vcpkg_list(APPEND linker_flags "${libpath}") - z_vcpkg_meson_convert_list_to_python_array(linker_flags ${linker_flags}) - string(APPEND MESON_${lang_mapping}FLAGS "${langlower}_link_args = ${linker_flags}\n") - set(MESON_${lang_mapping}FLAGS "${MESON_${lang_mapping}FLAGS}" PARENT_SCOPE) - endforeach() -endfunction() - -function(z_vcpkg_get_build_and_host_system build_system host_system is_cross) #https://mesonbuild.com/Cross-compilation.html - set(build_unknown FALSE) - if(CMAKE_HOST_WIN32) - if(DEFINED ENV{PROCESSOR_ARCHITEW6432}) - set(build_arch $ENV{PROCESSOR_ARCHITEW6432}) - else() - set(build_arch $ENV{PROCESSOR_ARCHITECTURE}) - endif() - if(build_arch MATCHES "(amd|AMD)64") - set(build_cpu_fam x86_64) - set(build_cpu x86_64) - elseif(build_arch MATCHES "(x|X)86") - set(build_cpu_fam x86) - set(build_cpu i686) - elseif(build_arch MATCHES "^(ARM|arm)64$") - set(build_cpu_fam aarch64) - set(build_cpu armv8) - elseif(build_arch MATCHES "^(ARM|arm)$") - set(build_cpu_fam arm) - set(build_cpu armv7hl) - else() - if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) - message(WARNING "Unsupported build architecture ${build_arch}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") - endif() - set(build_unknown TRUE) - endif() - elseif(CMAKE_HOST_UNIX) - # at this stage, CMAKE_HOST_SYSTEM_PROCESSOR is not defined - execute_process( - COMMAND uname -m - OUTPUT_VARIABLE MACHINE - OUTPUT_STRIP_TRAILING_WHITESPACE - COMMAND_ERROR_IS_FATAL ANY) - - # Show real machine architecture to visually understand whether we are in a native Apple Silicon terminal or running under Rosetta emulation - debug_message("Machine: ${MACHINE}") - - if(MACHINE MATCHES "arm64|aarch64") - set(build_cpu_fam aarch64) - set(build_cpu armv8) - elseif(MACHINE MATCHES "armv7h?l") - set(build_cpu_fam arm) - set(build_cpu ${MACHINE}) - elseif(MACHINE MATCHES "x86_64|amd64") - set(build_cpu_fam x86_64) - set(build_cpu x86_64) - elseif(MACHINE MATCHES "x86|i686") - set(build_cpu_fam x86) - set(build_cpu i686) - elseif(MACHINE MATCHES "i386") - set(build_cpu_fam x86) - set(build_cpu i386) - elseif(MACHINE MATCHES "loongarch64") - set(build_cpu_fam loongarch64) - set(build_cpu loongarch64) - else() - # https://github.com/mesonbuild/meson/blob/master/docs/markdown/Reference-tables.md#cpu-families - if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) - message(WARNING "Unhandled machine: ${MACHINE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") - endif() - set(build_unknown TRUE) - endif() - else() - if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) - message(WARNING "Failed to detect the build architecture! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") - endif() - set(build_unknown TRUE) - endif() - - set(build "[build_machine]\n") # Machine the build is performed on - string(APPEND build "endian = 'little'\n") - if(CMAKE_HOST_WIN32) - string(APPEND build "system = 'windows'\n") - elseif(CMAKE_HOST_APPLE) - string(APPEND build "system = 'darwin'\n") - elseif(CYGWIN) - string(APPEND build "system = 'cygwin'\n") - elseif(CMAKE_HOST_UNIX) - string(APPEND build "system = 'linux'\n") - else() - set(build_unknown TRUE) - endif() - - if(DEFINED build_cpu_fam) - string(APPEND build "cpu_family = '${build_cpu_fam}'\n") - endif() - if(DEFINED build_cpu) - string(APPEND build "cpu = '${build_cpu}'") - endif() - if(NOT build_unknown) - set(${build_system} "${build}" PARENT_SCOPE) - endif() - - set(host_unkown FALSE) - if(VCPKG_TARGET_ARCHITECTURE MATCHES "(amd|AMD|x|X)64") - set(host_cpu_fam x86_64) - set(host_cpu x86_64) - elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "(x|X)86") - set(host_cpu_fam x86) - set(host_cpu i686) - elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") - set(host_cpu_fam aarch64) - set(host_cpu armv8) - elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)$") - set(host_cpu_fam arm) - set(host_cpu armv7hl) - elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "loongarch64") - set(host_cpu_fam loongarch64) - set(host_cpu loongarch64) - elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "wasm32") - set(host_cpu_fam wasm32) - set(host_cpu wasm32) - else() - if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) - message(WARNING "Unsupported target architecture ${VCPKG_TARGET_ARCHITECTURE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the host_machine entry!" ) - endif() - set(host_unkown TRUE) - endif() - - set(host "[host_machine]\n") # host=target in vcpkg. - string(APPEND host "endian = 'little'\n") - if(NOT VCPKG_CMAKE_SYSTEM_NAME OR VCPKG_TARGET_IS_MINGW OR VCPKG_TARGET_IS_UWP) - set(meson_system_name "windows") - else() - string(TOLOWER "${VCPKG_CMAKE_SYSTEM_NAME}" meson_system_name) - endif() - string(APPEND host "system = '${meson_system_name}'\n") - string(APPEND host "cpu_family = '${host_cpu_fam}'\n") - string(APPEND host "cpu = '${host_cpu}'") - if(NOT host_unkown) - set(${host_system} "${host}" PARENT_SCOPE) - endif() - - if(NOT build_cpu_fam MATCHES "${host_cpu_fam}" - OR VCPKG_TARGET_IS_ANDROID OR VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_UWP - OR (VCPKG_TARGET_IS_MINGW AND NOT CMAKE_HOST_WIN32)) - set(${is_cross} TRUE PARENT_SCOPE) - endif() -endfunction() - -function(z_vcpkg_meson_setup_extra_windows_variables config_type) - ## b_vscrt - if(VCPKG_CRT_LINKAGE STREQUAL "static") - set(crt_type "mt") - else() - set(crt_type "md") - endif() - if(config_type STREQUAL "DEBUG") - set(crt_type "${crt_type}d") - endif() - set(MESON_VSCRT_LINKAGE "b_vscrt = '${crt_type}'" PARENT_SCOPE) - ## winlibs - separate_arguments(c_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_C_STANDARD_LIBRARIES}") - separate_arguments(cpp_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_CXX_STANDARD_LIBRARIES}") - z_vcpkg_meson_convert_list_to_python_array(c_winlibs ${c_winlibs}) - z_vcpkg_meson_convert_list_to_python_array(cpp_winlibs ${cpp_winlibs}) - set(MESON_WINLIBS "c_winlibs = ${c_winlibs}\n") - string(APPEND MESON_WINLIBS "cpp_winlibs = ${cpp_winlibs}") - set(MESON_WINLIBS "${MESON_WINLIBS}" PARENT_SCOPE) -endfunction() - -function(z_vcpkg_meson_setup_variables config_type) - set(meson_var_list VSCRT_LINKAGE WINLIBS MT AR RC C C_LD CXX CXX_LD OBJC OBJC_LD OBJCXX OBJCXX_LD FC FC_LD WINDRES CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS FCFLAGS SHARED_LINKER_FLAGS) - foreach(var IN LISTS meson_var_list) - set(MESON_${var} "") - endforeach() - - if(VCPKG_TARGET_IS_WINDOWS) - z_vcpkg_meson_setup_extra_windows_variables("${config_type}") - endif() - - z_vcpkg_meson_set_proglist_variables("${config_type}") - z_vcpkg_meson_set_flags_variables("${config_type}") - - foreach(var IN LISTS meson_var_list) - set(MESON_${var} "${MESON_${var}}" PARENT_SCOPE) - endforeach() -endfunction() - -function(vcpkg_generate_meson_cmd_args) - cmake_parse_arguments(PARSE_ARGV 0 arg - "" - "OUTPUT;CONFIG" - "OPTIONS;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_PROPERTIES" - ) - - if(NOT arg_LANGUAGES) - set(arg_LANGUAGES C CXX) - endif() - - vcpkg_list(JOIN arg_ADDITIONAL_BINARIES "\n" MESON_ADDITIONAL_BINARIES) - vcpkg_list(JOIN arg_ADDITIONAL_PROPERTIES "\n" MESON_ADDITIONAL_PROPERTIES) - - set(buildtype "${arg_CONFIG}") - - if(NOT VCPKG_CHAINLOAD_TOOLCHAIN_FILE) - z_vcpkg_select_default_vcpkg_chainload_toolchain() - endif() - vcpkg_list(APPEND VCPKG_CMAKE_CONFIGURE_OPTIONS "-DVCPKG_LANGUAGES=${arg_LANGUAGES}") - vcpkg_cmake_get_vars(cmake_vars_file) - debug_message("Including cmake vars from: ${cmake_vars_file}") - include("${cmake_vars_file}") - - vcpkg_list(APPEND arg_OPTIONS --backend ninja --wrap-mode nodownload -Doptimization=plain) - - z_vcpkg_get_build_and_host_system(MESON_HOST_MACHINE MESON_BUILD_MACHINE IS_CROSS) - - if(arg_CONFIG STREQUAL "DEBUG") - set(suffix "dbg") - else() - string(SUBSTRING "${arg_CONFIG}" 0 3 suffix) - string(TOLOWER "${suffix}" suffix) - endif() - set(meson_input_file_${buildtype} "${CURRENT_BUILDTREES_DIR}/meson-${TARGET_TRIPLET}-${suffix}.log") - - if(IS_CROSS) - # VCPKG_CROSSCOMPILING is not used since it regresses a lot of ports in x64-windows-x triplets - # For consistency this should proably be changed in the future? - vcpkg_list(APPEND arg_OPTIONS --native "${SCRIPTS}/buildsystems/meson/none.txt") - vcpkg_list(APPEND arg_OPTIONS --cross "${meson_input_file_${buildtype}}") - else() - vcpkg_list(APPEND arg_OPTIONS --native "${meson_input_file_${buildtype}}") - endif() - - # User provided cross/native files - if(VCPKG_MESON_NATIVE_FILE) - vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE}") - endif() - if(VCPKG_MESON_NATIVE_FILE_${buildtype}) - vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE_${buildtype}}") - endif() - if(VCPKG_MESON_CROSS_FILE) - vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE}") - endif() - if(VCPKG_MESON_CROSS_FILE_${buildtype}) - vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE_${buildtype}}") - endif() - - vcpkg_list(APPEND arg_OPTIONS --libdir lib) # else meson install into an architecture describing folder - vcpkg_list(APPEND arg_OPTIONS --pkgconfig.relocatable) - - if(arg_CONFIG STREQUAL "RELEASE") - vcpkg_list(APPEND arg_OPTIONS -Ddebug=false --prefix "${CURRENT_PACKAGES_DIR}") - vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") - if(VCPKG_TARGET_IS_WINDOWS) - vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}/share']") - else() - vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug']") - endif() - elseif(arg_CONFIG STREQUAL "DEBUG") - vcpkg_list(APPEND arg_OPTIONS -Ddebug=true --prefix "${CURRENT_PACKAGES_DIR}/debug" --includedir ../include) - vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") - if(VCPKG_TARGET_IS_WINDOWS) - vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/share']") - else() - vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}']") - endif() - else() - message(FATAL_ERROR "Unknown configuration. Only DEBUG and RELEASE are valid values.") - endif() - - # Allow overrides / additional configuration variables from triplets - if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS) - vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS}) - endif() - if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}) - vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}}) - endif() - - if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") - set(MESON_DEFAULT_LIBRARY shared) - else() - set(MESON_DEFAULT_LIBRARY static) - endif() - set(MESON_CMAKE_BUILD_TYPE "${cmake_build_type_${buildtype}}") - z_vcpkg_meson_setup_variables(${buildtype}) - configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/meson.template.in" "${meson_input_file_${buildtype}}" @ONLY) - set("${arg_OUTPUT}" ${arg_OPTIONS} PARENT_SCOPE) -endfunction() - -function(vcpkg_configure_meson) - # parse parameters such that semicolons in options arguments to COMMAND don't get erased - cmake_parse_arguments(PARSE_ARGV 0 arg - "NO_PKG_CONFIG" - "SOURCE_PATH" - "OPTIONS;OPTIONS_DEBUG;OPTIONS_RELEASE;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_NATIVE_BINARIES;ADDITIONAL_CROSS_BINARIES;ADDITIONAL_PROPERTIES" - ) - - if(DEFINED arg_ADDITIONAL_NATIVE_BINARIES OR DEFINED arg_ADDITIONAL_CROSS_BINARIES) - message(WARNING "Options ADDITIONAL_(NATIVE|CROSS)_BINARIES have been deprecated. Only use ADDITIONAL_BINARIES!") - endif() - vcpkg_list(APPEND arg_ADDITIONAL_BINARIES ${arg_ADDITIONAL_NATIVE_BINARIES} ${arg_ADDITIONAL_CROSS_BINARIES}) - vcpkg_list(REMOVE_DUPLICATES arg_ADDITIONAL_BINARIES) - - file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") - file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") - - vcpkg_find_acquire_program(MESON) - - get_filename_component(CMAKE_PATH "${CMAKE_COMMAND}" DIRECTORY) - vcpkg_add_to_path("${CMAKE_PATH}") # Make CMake invokeable for Meson - - vcpkg_find_acquire_program(NINJA) - - if(NOT arg_NO_PKG_CONFIG) - vcpkg_find_acquire_program(PKGCONFIG) - set(ENV{PKG_CONFIG} "${PKGCONFIG}") - endif() - - vcpkg_find_acquire_program(PYTHON3) - get_filename_component(PYTHON3_DIR "${PYTHON3}" DIRECTORY) - vcpkg_add_to_path(PREPEND "${PYTHON3_DIR}") - - set(buildtypes "") - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") - set(buildname "DEBUG") - set(cmake_build_type_${buildname} "Debug") - vcpkg_list(APPEND buildtypes "${buildname}") - set(path_suffix_${buildname} "debug/") - set(suffix_${buildname} "dbg") - endif() - if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") - set(buildname "RELEASE") - set(cmake_build_type_${buildname} "Release") - vcpkg_list(APPEND buildtypes "${buildname}") - set(path_suffix_${buildname} "") - set(suffix_${buildname} "rel") - endif() - - # configure build - foreach(buildtype IN LISTS buildtypes) - message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}}") - file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}") - - vcpkg_generate_meson_cmd_args( - OUTPUT cmd_args - CONFIG ${buildtype} - LANGUAGES ${arg_LANGUAGES} - OPTIONS ${arg_OPTIONS} ${arg_OPTIONS_${buildtype}} - ADDITIONAL_BINARIES ${arg_ADDITIONAL_BINARIES} - ADDITIONAL_PROPERTIES ${arg_ADDITIONAL_PROPERTIES} - ) - - vcpkg_execute_required_process( - COMMAND ${MESON} setup ${cmd_args} ${arg_SOURCE_PATH} - WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}" - LOGNAME config-${TARGET_TRIPLET}-${suffix_${buildtype}} - SAVE_LOG_FILES - meson-logs/meson-log.txt - meson-info/intro-dependencies.json - meson-logs/install-log.txt - ) - - message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}} done") - endforeach() -endfunction() diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake deleted file mode 100644 index 0351f271a..000000000 --- a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake +++ /dev/null @@ -1,71 +0,0 @@ -function(vcpkg_install_meson) - cmake_parse_arguments(PARSE_ARGV 0 arg "ADD_BIN_TO_PATH" "" "") - - vcpkg_find_acquire_program(NINJA) - unset(ENV{DESTDIR}) # installation directory was already specified with '--prefix' option - - if(VCPKG_TARGET_IS_OSX) - vcpkg_backup_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) - set(ENV{SDKROOT} "${VCPKG_DETECTED_CMAKE_OSX_SYSROOT}") - set(ENV{MACOSX_DEPLOYMENT_TARGET} "${VCPKG_DETECTED_CMAKE_OSX_DEPLOYMENT_TARGET}") - endif() - - foreach(buildtype IN ITEMS "debug" "release") - if(DEFINED VCPKG_BUILD_TYPE AND NOT VCPKG_BUILD_TYPE STREQUAL buildtype) - continue() - endif() - - if(buildtype STREQUAL "debug") - set(short_buildtype "dbg") - else() - set(short_buildtype "rel") - endif() - - message(STATUS "Package ${TARGET_TRIPLET}-${short_buildtype}") - if(arg_ADD_BIN_TO_PATH) - vcpkg_backup_env_variables(VARS PATH) - if(buildtype STREQUAL "debug") - vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/debug/bin") - else() - vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/bin") - endif() - endif() - vcpkg_execute_required_process( - COMMAND "${NINJA}" install -v - WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${short_buildtype}" - LOGNAME package-${TARGET_TRIPLET}-${short_buildtype} - ) - if(arg_ADD_BIN_TO_PATH) - vcpkg_restore_env_variables(VARS PATH) - endif() - endforeach() - - vcpkg_list(SET renamed_libs) - if(VCPKG_TARGET_IS_WINDOWS AND VCPKG_LIBRARY_LINKAGE STREQUAL static AND NOT VCPKG_TARGET_IS_MINGW) - # Meson names all static libraries lib.a which basically breaks the world - file(GLOB_RECURSE gen_libraries "${CURRENT_PACKAGES_DIR}*/**/lib*.a") - foreach(gen_library IN LISTS gen_libraries) - get_filename_component(libdir "${gen_library}" DIRECTORY) - get_filename_component(libname "${gen_library}" NAME) - string(REGEX REPLACE ".a$" ".lib" fixed_librawname "${libname}") - string(REGEX REPLACE "^lib" "" fixed_librawname "${fixed_librawname}") - file(RENAME "${gen_library}" "${libdir}/${fixed_librawname}") - # For cmake fixes. - string(REGEX REPLACE ".a$" "" origin_librawname "${libname}") - string(REGEX REPLACE ".lib$" "" fixed_librawname "${fixed_librawname}") - vcpkg_list(APPEND renamed_libs ${fixed_librawname}) - set(${librawname}_old ${origin_librawname}) - set(${librawname}_new ${fixed_librawname}) - endforeach() - file(GLOB_RECURSE cmake_files "${CURRENT_PACKAGES_DIR}*/*.cmake") - foreach(cmake_file IN LISTS cmake_files) - foreach(current_lib IN LISTS renamed_libs) - vcpkg_replace_string("${cmake_file}" "${${current_lib}_old}" "${${current_lib}_new}" IGNORE_UNCHANGED) - endforeach() - endforeach() - endif() - - if(VCPKG_TARGET_IS_OSX) - vcpkg_restore_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) - endif() -endfunction() diff --git a/cmake/vcpkg-triplets/universal-osx.cmake b/cmake/vcpkg-triplets/universal-osx.cmake deleted file mode 100644 index 1c91a5650..000000000 --- a/cmake/vcpkg-triplets/universal-osx.cmake +++ /dev/null @@ -1,8 +0,0 @@ -# See https://github.com/microsoft/vcpkg/discussions/19454 -# NOTE: Try to keep in sync with default arm64-osx definition -set(VCPKG_TARGET_ARCHITECTURE x64) -set(VCPKG_CRT_LINKAGE dynamic) -set(VCPKG_LIBRARY_LINKAGE static) - -set(VCPKG_CMAKE_SYSTEM_NAME Darwin) -set(VCPKG_OSX_ARCHITECTURES "arm64;x86_64") diff --git a/default.nix b/default.nix index 5ecef5590..6466507b7 100644 --- a/default.nix +++ b/default.nix @@ -1,4 +1,9 @@ -(import (fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; - sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; -}) { src = ./.; }).defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index 640d2bcf1..a82e6f65f 100644 --- a/flake.lock +++ b/flake.lock @@ -1,13 +1,29 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "libnbtplusplus": { "flake": false, "locked": { - "lastModified": 1772016279, - "narHash": "sha256-7itkptyjoRcXfGLwg1/jxajetZ3a4mDc66+w4X6yW8s=", + "lastModified": 1699286814, + "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=", "owner": "PrismLauncher", "repo": "libnbtplusplus", - "rev": "687e43031df0dc641984b4256bcca50d5b3f7de3", + "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad", "type": "github" }, "original": { @@ -16,22 +32,42 @@ "type": "github" } }, + "nix-filter": { + "locked": { + "lastModified": 1710156097, + "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "3342559a24e85fc164b295c3444e8a139924675b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1778443072, - "narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=", - "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", - "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz" + "lastModified": 1729256560, + "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" } }, "root": { "inputs": { + "flake-compat": "flake-compat", "libnbtplusplus": "libnbtplusplus", + "nix-filter": "nix-filter", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index 289e0ec1c..54add656d 100644 --- a/flake.nix +++ b/flake.nix @@ -9,12 +9,34 @@ }; inputs = { - nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; + + nix-filter.url = "github:numtide/nix-filter"; + + /* + Inputs below this are optional and can be removed + + ``` + { + inputs.prismlauncher = { + url = "github:PrismLauncher/PrismLauncher"; + inputs = { + flake-compat.follows = ""; + }; + }; + } + ``` + */ + + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; }; outputs = @@ -22,8 +44,9 @@ self, nixpkgs, libnbtplusplus, + nix-filter, + ... }: - let inherit (nixpkgs) lib; @@ -35,178 +58,47 @@ forAllSystems = lib.genAttrs systems; nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); in - { checks = forAllSystems ( system: - let - pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_22; + checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; }; in - - { - formatting = - pkgs.runCommand "check-formatting" - { - nativeBuildInputs = with pkgs; [ - deadnix - llvm.clang-tools - markdownlint-cli - nixfmt-rfc-style - statix - ]; - } - '' - cd ${self} - - echo "Running clang-format...." - clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} - - echo "Running deadnix..." - deadnix --fail - - echo "Running markdownlint..." - markdownlint --dot . - - echo "Running nixfmt..." - find -type f -name '*.nix' -exec nixfmt --check {} + - - echo "Running statix" - statix check . - - touch $out - ''; - } + lib.filterAttrs (_: lib.isDerivation) checks' ); devShells = forAllSystems ( system: - let pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_22; - python = pkgs.python3; - mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; }; - - packages' = self.packages.${system}; - - welcomeMessage = '' - Welcome to the Prism Launcher repository! 🌈 - - We just set some things up for you. To get building, you can run: - - ``` - $ cd "$cmakeBuildDir" - $ ninjaBuildPhase - $ ninjaInstallPhase - ``` - - Feel free to ask any questions in our Discord server or Matrix space: - - https://prismlauncher.org/discord - - https://matrix.to/#/#prismlauncher:matrix.org - - And thanks for helping out :) - ''; - - # Re-use our package wrapper to wrap our development environment - qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: { - name = "qt-wrapper-env"; - - # Required to use script-based makeWrapper below - strictDeps = true; - - # We don't need/want the unwrapped Prism package - paths = [ ]; - - nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [ - # Ensure the wrapper is script based so it can be sourced - pkgs.makeWrapper - ]; - - # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10 - buildCommand = '' - makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out" - sed -i '/^exec/d' "$out" - ''; - }); in - { - default = mkShell { - name = "prism-launcher"; - - inputsFrom = [ packages'.prismlauncher-unwrapped ]; - - 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"; - }) + default = pkgs.mkShell { + inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ]; + buildInputs = with pkgs; [ + ccache + ninja ]; - - cmakeBuildType = "Debug"; - cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher-unwrapped.cmakeFlags; - dontFixCmake = true; - - shellHook = '' - echo "Sourcing ${qt-wrapper-env}" - source ${qt-wrapper-env} - - git submodule update --init --force - - if [ ! -f compile_commands.json ]; then - cmakeConfigurePhase - cd .. - ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json - fi - - echo ${lib.escapeShellArg welcomeMessage} - ''; }; } ); formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); - overlays.default = - final: prev: - - let - 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 { }; + overlays.default = final: prev: { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit + libnbtplusplus + nix-filter + self + ; }; + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; + packages = forAllSystems ( system: - let pkgs = nixpkgsFor.${system}; @@ -219,7 +111,6 @@ default = prismPackages.prismlauncher; }; in - # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); @@ -227,18 +118,16 @@ # We put these under legacyPackages as they are meant for CI, not end user consumption legacyPackages = forAllSystems ( system: - let - packages' = self.packages.${system}; - legacyPackages' = self.legacyPackages.${system}; + prismPackages = self.packages.${system}; + legacyPackages = self.legacyPackages.${system}; in - { - prismlauncher-debug = packages'.prismlauncher.override { - prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug; + prismlauncher-debug = prismPackages.prismlauncher.override { + prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; }; - prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs { + prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { cmakeBuildType = "Debug"; dontStrip = true; }; diff --git a/flatpak/flite.json b/flatpak/flite.json new file mode 100644 index 000000000..1bf280af1 --- /dev/null +++ b/flatpak/flite.json @@ -0,0 +1,20 @@ +{ + "name": "flite", + "config-opts": [ + "--enable-shared", + "--with-audio=pulseaudio" + ], + "no-parallel-make": true, + "sources": [ + { + "type": "git", + "url": "https://github.com/festvox/flite.git", + "tag": "v2.2", + "commit": "e9e2e37c329dbe98bfeb27a1828ef9a71fa84f88", + "x-checker-data": { + "type": "git", + "tag-pattern": "^v([\\d.]+)$" + } + } + ] +} diff --git a/flatpak/libdecor.json b/flatpak/libdecor.json new file mode 100644 index 000000000..1652a2f04 --- /dev/null +++ b/flatpak/libdecor.json @@ -0,0 +1,18 @@ +{ + "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 new file mode 100644 index 000000000..136aef91a --- /dev/null +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -0,0 +1,151 @@ +id: org.prismlauncher.PrismLauncher +runtime: org.kde.Platform +runtime-version: '6.8' +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: + # 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 new file mode 100644 index 000000000..70cec9981 --- /dev/null +++ b/flatpak/patches/0009-Defer-setting-cursor-position-until-the-cursor-is-lo.patch @@ -0,0 +1,59 @@ +From 9997ae55a47de469ea26f8437c30b51483abda5f Mon Sep 17 00:00:00 2001 +From: Dan Klishch +Date: Sat, 30 Sep 2023 23:38:05 -0400 +Subject: Defer setting cursor position until the cursor is locked + +--- + src/wl_platform.h | 3 +++ + src/wl_window.c | 14 ++++++++++++-- + 2 files changed, 15 insertions(+), 2 deletions(-) + +diff --git a/src/wl_platform.h b/src/wl_platform.h +index ca34f66e..cd1f227f 100644 +--- a/src/wl_platform.h ++++ b/src/wl_platform.h +@@ -403,6 +403,9 @@ typedef struct _GLFWwindowWayland + int scaleSize; + int compositorPreferredScale; + ++ double askedCursorPosX, askedCursorPosY; ++ GLFWbool didAskForSetCursorPos; ++ + struct zwp_relative_pointer_v1* relativePointer; + struct zwp_locked_pointer_v1* lockedPointer; + struct zwp_confined_pointer_v1* confinedPointer; +diff --git a/src/wl_window.c b/src/wl_window.c +index 1de26558..0df16747 100644 +--- a/src/wl_window.c ++++ b/src/wl_window.c +@@ -2586,8 +2586,9 @@ void _glfwGetCursorPosWayland(_GLFWwindow* window, double* xpos, double* ypos) + + void _glfwSetCursorPosWayland(_GLFWwindow* window, double x, double y) + { +- _glfwInputError(GLFW_FEATURE_UNAVAILABLE, +- "Wayland: The platform does not support setting the cursor position"); ++ window->wl.didAskForSetCursorPos = true; ++ window->wl.askedCursorPosX = x; ++ window->wl.askedCursorPosY = y; + } + + void _glfwSetCursorModeWayland(_GLFWwindow* window, int mode) +@@ -2819,6 +2820,15 @@ static const struct zwp_relative_pointer_v1_listener relativePointerListener = + static void lockedPointerHandleLocked(void* userData, + struct zwp_locked_pointer_v1* lockedPointer) + { ++ _GLFWwindow* window = userData; ++ ++ if (window->wl.didAskForSetCursorPos) ++ { ++ window->wl.didAskForSetCursorPos = false; ++ zwp_locked_pointer_v1_set_cursor_position_hint(window->wl.lockedPointer, ++ wl_fixed_from_double(window->wl.askedCursorPosX), ++ wl_fixed_from_double(window->wl.askedCursorPosY)); ++ } + } + + static void lockedPointerHandleUnlocked(void* userData, +-- +2.42.0 + diff --git a/flatpak/prime-run b/flatpak/prime-run new file mode 100644 index 000000000..946c28dd5 --- /dev/null +++ b/flatpak/prime-run @@ -0,0 +1,4 @@ +#!/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 new file mode 100644 index 000000000..039d890d2 --- /dev/null +++ b/flatpak/prismlauncher @@ -0,0 +1,11 @@ +#!/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 new file mode 160000 index 000000000..f2b0c16a2 --- /dev/null +++ b/flatpak/shared-modules @@ -0,0 +1 @@ +Subproject commit f2b0c16a2a217a1822ce5a6538ba8f755ed1dd32 diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ddeb30588..1d5efe7fc 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -46,12 +46,12 @@ #include "DataMigrationTask.h" #include "java/JavaInstallList.h" #include "net/PasteUpload.h" +#include "pathmatcher/MultiMatcher.h" +#include "pathmatcher/SimplePrefixMatcher.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" -#include "ui/ToolTipFilter.h" -#include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" @@ -59,7 +59,8 @@ #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" -#include "ui/pages/global/AppearancePage.h" +#include "ui/pages/global/CustomCommandsPage.h" +#include "ui/pages/global/EnvironmentVariablesPage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" @@ -97,7 +98,6 @@ #include #include #include -#include #include #include #include @@ -109,6 +109,8 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" +#include "java/JavaInstallList.h" + #include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" @@ -126,11 +128,12 @@ #include #include +#include #include "SysInfo.h" #ifdef Q_OS_LINUX #include -#include "LibraryUtils.h" +#include "MangoHud.h" #include "gamemode_client.h" #endif @@ -152,15 +155,10 @@ #endif #if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif #include -#include +#include "WindowsConsole.h" #endif -#include "console/Console.h" - #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) @@ -168,63 +166,6 @@ static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; -static bool isANSIColorConsole; - -static QString defaultLogFormat = QStringLiteral( - "%{time process}" - " " - "%{if-debug}Debug:%{endif}" - "%{if-info}Info:%{endif}" - "%{if-warning}Warning:%{endif}" - "%{if-critical}Critical:%{endif}" - "%{if-fatal}Fatal:%{endif}" - " " - "%{if-category}[%{category}] %{endif}" - "%{message}" - " " - "(%{function}:%{line})"); - -#define ansi_reset "\x1b[0m" -#define ansi_bold "\x1b[1m" -#define ansi_reset_bold "\x1b[22m" -#define ansi_faint "\x1b[2m" -#define ansi_italic "\x1b[3m" -#define ansi_red_fg "\x1b[31m" -#define ansi_green_fg "\x1b[32m" -#define ansi_yellow_fg "\x1b[33m" -#define ansi_blue_fg "\x1b[34m" -#define ansi_purple_fg "\x1b[35m" -#define ansi_inverse "\x1b[7m" - -// clang-format off -static QString ansiLogFormat = QStringLiteral( - ansi_faint "%{time process}" ansi_reset - " " - "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" - "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" - "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" - "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" - "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" - " " - "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" - "%{message}" - " " - ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset -); -// clang-format on - -#undef ansi_inverse -#undef ansi_purple_fg -#undef ansi_blue_fg -#undef ansi_yellow_fg -#undef ansi_green_fg -#undef ansi_red_fg -#undef ansi_italic -#undef ansi_faint -#undef ansi_bold -#undef ansi_reset_bold -#undef ansi_reset - namespace { /** This is used so that we can output to the log file in addition to the CLI. */ @@ -233,27 +174,11 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe - if (isANSIColorConsole) { - // ensure default is set for log file - qSetMessagePattern(defaultLogFormat); - } - QString out = qFormatLogMessage(type, context, msg); - if (APPLICATION->logModel) { - APPLICATION->logModel->append(MessageLevel::fromQtMsgType(type), out); - } - out += QChar::LineFeed; + APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); - - if (isANSIColorConsole) { - // format ansi for console; - qSetMessagePattern(ansiLogFormat); - out = qFormatLogMessage(type, context, msg); - out += QChar::LineFeed; - } - QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } @@ -290,17 +215,19 @@ std::tuple read_lock_File(const Q Application::Application(int& argc, char** argv) : QApplication(argc, argv) { - if (console::isConsole()) { - isANSIColorConsole = true; +#if defined Q_OS_WIN32 + // attach the parent console if stdout not already captured + if (AttachWindowsConsole()) { + consoleAttached = true; } - +#endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); - setDesktopFileName(BuildConfig.LAUNCHER_APPID); - m_startTime = QDateTime::currentDateTime(); + setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); + startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); @@ -316,9 +243,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, - { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, - { "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 @@ -333,14 +258,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_serverToJoin = parser.value("server"); m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); - if (parser.isSet("offline")) { - m_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)); @@ -352,9 +272,8 @@ 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_launchOffline) && - m_instanceIdToLaunch.isEmpty()) { - std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; + if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { + std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; } @@ -394,7 +313,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } else { QDir foo; if (DesktopServices::isSnap()) { - foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON")); + foo = QDir(getenv("SNAP_USER_COMMON")); } else { foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); } @@ -451,20 +370,19 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { - bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; - sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); + m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); - sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); + m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { @@ -480,20 +398,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } - if (m_launchOffline) { - launch.args["offline_enabled"] = "true"; - launch.args["offline_name"] = m_offlineName; - } - sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); - } - if (sentMessage) { - m_status = Application::Succeeded; - return; - } else { - std::cerr << "Unable to redirect command to already running instance\n"; - // C function not Qt function - event loop not started yet - ::exit(1); + m_peerInstance->sendMessage(launch.serialize(), timeout); } + m_status = Application::Succeeded; + return; } } @@ -514,27 +422,37 @@ 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 - %1.\n" + QString("The launcher couldn't create a log file - the data folder is not writable.\n" "\n" "Make sure you have write permissions to the data folder.\n" - "(%2)\n" + "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") - .arg(logFile->errorString()) .arg(dataPath)); return; } qInstallMessageHandler(appDebugOutput); - qSetMessagePattern(defaultLogFormat); - logModel.reset(new LogModel(this)); + qSetMessagePattern( + "%{time process}" + " " + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + " " + "|" + " " + "%{if-category}[%{category}]: %{endif}" + "%{message}"); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); - qInfo() << "Testing" << logRulesPath << "..."; + qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() @@ -542,7 +460,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { - qInfo() << "Found" << logRulesPath << "..."; + qDebug() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } @@ -553,69 +471,71 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif - qInfo() << "Testing" << logRulesPath << "..."; + qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules - qInfo() << "Loading logging rules from:" << logRulesPath; + qDebug() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; - qInfo() << "Setting log rules:"; + qDebug() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); - qInfo() << " " << rule; + qDebug() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } - qInfo() << "<> Log initialized."; + qDebug() << "<> Log initialized."; } { - 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"); - } + 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"); } { - 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"); + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qDebug() << "Version : " << BuildConfig.printableVersionString(); + qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; + qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; + qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qDebug() << "Compiled for : " << BuildConfig.systemID(); + qDebug() << "Compiled by : " << BuildConfig.compilerID(); + qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { - qInfo() << "Work dir before adjustment :" << origcwdPath; - qInfo() << "Work dir after adjustment :" << QDir::currentPath(); - qInfo() << "Adjusted by :" << adjustedBy; + qDebug() << "Work dir before adjustment : " << origcwdPath; + qDebug() << "Work dir after adjustment : " << QDir::currentPath(); + qDebug() << "Adjusted by : " << adjustedBy; } else { - qInfo() << "Work dir :" << QDir::currentPath(); + qDebug() << "Work dir : " << QDir::currentPath(); } - qInfo() << "Binary path :" << binPath; - qInfo() << "Application root path :" << m_rootPath; + qDebug() << "Binary path : " << binPath; + qDebug() << "Application root path : " << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { - qInfo() << "ID of instance to launch :" << m_instanceIdToLaunch; + qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { - qInfo() << "Address of server to join :" << m_serverToJoin; + qDebug() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { - qInfo() << "Name of the world to join :" << m_worldToJoin; + qDebug() << "Name of the world to join :" << m_worldToJoin; } - qInfo() << "<> Paths set."; + qDebug() << "<> Paths set."; } if (m_liveCheck) { @@ -625,11 +545,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 << "error:" << check.errorString(); + qWarning() << "Could not write into" << liveCheckFile << "!"; check.remove(); // also closes file! } } else { - qWarning() << "Could not open" << liveCheckFile << "for writing:" << check.errorString(); + qWarning() << "Could not open" << liveCheckFile << "for writing!"; } } @@ -672,38 +592,23 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QFontInfo consoleFontInfo(consoleFont); QString resolvedDefaultMonospace = consoleFontInfo.family(); QFont resolvedFont(resolvedDefaultMonospace); - qDebug().nospace() << "Detected default console font: " << resolvedDefaultMonospace - << ", substitutions: " << resolvedFont.substitutions().join(','); + qDebug() << "Detected default console font:" << resolvedDefaultMonospace + << ", substitutions:" << resolvedFont.substitutions().join(','); m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); m_settings->registerSetting("ConsoleFontSize", defaultSize); m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); - logModel->setMaxLines(getConsoleMaxLines(settings())); - logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); - logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); - // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); - m_settings->registerSetting("MoveModsFromDownloadsDir", false); m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("JavaDir", "java"); -#ifdef Q_OS_MACOS - // Folder security-scoped bookmarks - m_settings->registerSetting("InstanceDirBookmark", ""); - m_settings->registerSetting("CentralModsDirBookmark", ""); - m_settings->registerSetting("IconsDirBookmark", ""); - m_settings->registerSetting("DownloadsDirBookmark", ""); - m_settings->registerSetting("SkinsDirBookmark", ""); - m_settings->registerSetting("JavaDirBookmark", ""); -#endif - // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -731,9 +636,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); - m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); m_settings->registerSetting("PermGen", 128); - m_settings->registerSetting("LowMemWarning", true); // Java Settings m_settings->registerSetting("JavaPath", ""); @@ -776,8 +680,6 @@ 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", ""); @@ -792,15 +694,12 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); m_settings->registerSetting("CatOpacity", 100); - m_settings->registerSetting("CatFit", "fit"); m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("ToolbarsLocked", false); - // Instance m_settings->registerSetting("InstSortMode", "Name"); - m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry @@ -818,17 +717,10 @@ 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", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); - m_settings->registerSetting("DataPackDownloadGeometry", ""); - - // data pack window - // in future, more pages may be added - so this name is chosen to avoid needing migration - m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { @@ -854,27 +746,20 @@ 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 - resetIfInvalid(m_settings->registerSetting("MetaURLOverride", "").get()); + m_settings->registerSetting("MetaURLOverride", ""); - // Resource URL - resetIfInvalid(m_settings->registerSetting({ "ResourceURLOverride", "ResourceURL" }, "").get()); + QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); - // Legacy FML libs URL - resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); + // get rid of invalid meta urls + if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) + m_settings->reset("MetaURLOverride"); } - m_settings->registerSetting("MetaRefreshOnLaunch", true); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); - m_settings->registerSetting("Env", "{}"); + m_settings->registerSetting("Env", QVariant(QMap())); // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); @@ -890,7 +775,6 @@ 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", ""); @@ -902,21 +786,22 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Init page provider { - m_globalSettingsProvider = std::make_unique(tr("Settings")); + m_globalSettingsProvider = std::make_shared(tr("Settings")); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); - qInfo() << "<> Settings loaded."; + qDebug() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY @@ -932,7 +817,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); - qInfo() << "<> Network done."; + qDebug() << "<> Network done."; + } + + // load translations + { + m_translations.reset(new TranslationsModel("translations")); + auto bcp47Name = m_settings->get("Language").toString(); + m_translations->selectLanguage(bcp47Name); + qDebug() << "Your language is" << bcp47Name; + qDebug() << "<> Translations loaded."; } // Instance icons @@ -942,53 +836,38 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) ":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" }; m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, - [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); - qInfo() << "<> Instance icons initialized."; + [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); + qDebug() << "<> Instance icons initialized."; } // Themes m_themeManager = std::make_unique(); -#ifdef Q_OS_MACOS - // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present - // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory, - // and a sandboxed version that can't access the directory without a bookmark - // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore - { - m_settings->get("InstanceDir"); - m_settings->get("CentralModsDir"); - m_settings->get("IconsDir"); - m_settings->get("DownloadsDir"); - m_settings->get("SkinsDir"); - m_settings->get("JavaDir"); - } -#endif - // initialize and load all instances { auto InstDirSetting = m_settings->getSetting("InstanceDir"); // 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; + QString instDir = InstDirSetting->get().toString(); + qDebug() << "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.get(), instDir, this)); + m_instances.reset(new InstanceList(m_settings, instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); - qInfo() << "Loading Instances..."; + qDebug() << "Loading Instances..."; m_instances->loadList(); - qInfo() << "<> Instances loaded."; + qDebug() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); - qInfo() << "Loading accounts..."; + qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); - qInfo() << "<> Accounts loaded."; + qDebug() << "<> Accounts loaded."; } // init the http meta cache @@ -1008,30 +887,24 @@ 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."; + qDebug() << "<> Cache initialized."; } - // load translations - { - m_translations.reset(new TranslationsModel("translations")); - m_translations->downloadIndex(); - qInfo() << "Your language is" << m_translations->selectedLanguage(); - qInfo() << "<> Translations loaded."; - } + // now we have network, download translation updates + m_translations->downloadIndex(); // 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.get()); + profiler->registerSettings(m_settings); } // Create the MCEdit thing... why is this here? { - m_mcedit.reset(new MCEditTool(m_settings.get())); + m_mcedit.reset(new MCEditTool(m_settings)); } #ifdef Q_OS_MACOS @@ -1188,10 +1061,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } } - if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { - installEventFilter(new ToolTipFilter); - } - if (createSetupWizard()) { return; } @@ -1202,11 +1071,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { - bool javaRequired = [this]() { - if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { + bool javaRequired = [&]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) { return false; } - bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); + bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } @@ -1220,8 +1089,8 @@ bool Application::createSetupWizard() QString actualPath = FS::ResolveExecutable(currentJavaPath); return actualPath.isNull(); }(); - bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() && - !settings()->get("AutomaticJavaSwitch").toBool() && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool(); + bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() && + !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); @@ -1233,16 +1102,8 @@ bool Application::createSetupWizard() // set default theme after going into theme wizard if (!validIcons) settings()->set("IconTheme", QString("pe_colored")); - if (!validWidgets) { -#if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - const QString style = - QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QStringLiteral("dark") : QStringLiteral("bright"); -#else - const QString style = QStringLiteral("system"); -#endif - - settings()->set("ApplicationTheme", style); - } + if (!validWidgets) + settings()->set("ApplicationTheme", QString("system")); m_themeManager->applyCurrentlySelectedTheme(true); @@ -1309,9 +1170,6 @@ bool Application::event(QEvent* event) #endif if (event->type() == QEvent::FileOpen) { - if (!m_mainWindow) { - showMainWindow(false); - } auto ev = static_cast(event); m_mainWindow->processURLs({ ev->url() }); } @@ -1352,11 +1210,8 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, m_launchOffline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); - - if (!m_showMainWindow) { - return; - } + launch(inst, true, false, targetToJoin, accountToUse); + return; } } if (!m_instanceIdToShowWindowOf.isEmpty()) { @@ -1409,6 +1264,16 @@ 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) @@ -1438,19 +1303,14 @@ void Application::messageReceived(const QByteArray& message) qWarning() << "Received" << command << "message without a zip path/URL."; return; } - if (!m_mainWindow) { - showMainWindow(false); - } m_mainWindow->processURLs({ normalizeImportUrl(url) }); } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; QString world = received.args["world"]; QString profile = received.args["profile"]; - bool offline = received.args["offline_enabled"] == "true"; - QString offlineName = received.args["offline_name"]; - BaseInstance* instance; + InstancePtr instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { @@ -1478,28 +1338,31 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName); + launch(instance, true, false, serverObject, accountObject); } else { qWarning() << "Received invalid message" << message; } } -TranslationsModel* Application::translations() +std::shared_ptr Application::translations() { - return m_translations.get(); + return m_translations; } -JavaInstallList* Application::javalist() +std::shared_ptr Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } - return m_javalist.get(); + return m_javalist; } -QIcon Application::logo() +QIcon Application::getThemedIcon(const QString& name) { - return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); + if (name == "logo") { + return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); + } + return QIcon::fromTheme(name); } bool Application::openJsonEditor(const QString& filename) @@ -1513,11 +1376,7 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(BaseInstance* instance, - LaunchMode mode, - MinecraftTarget::Ptr targetToJoin, - MinecraftAccountPtr accountToUse, - const QString& offlineName) +bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; @@ -1533,17 +1392,19 @@ bool Application::launch(BaseInstance* instance, auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); - controller->setLaunchMode(mode); + controller->setOnline(online); + controller->setDemo(demo); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); - controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } - connect(controller.get(), &LaunchController::finished, this, &Application::controllerFinished); + 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")); }); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; @@ -1557,7 +1418,7 @@ bool Application::launch(BaseInstance* instance, return false; } -bool Application::kill(BaseInstance* instance) +bool Application::kill(InstancePtr instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; @@ -1566,7 +1427,7 @@ bool Application::kill(BaseInstance* 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(); @@ -1615,19 +1476,18 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -void Application::controllerFinished() +void Application::controllerSucceeded() { - auto controller = qobject_cast(sender()); + auto controller = qobject_cast(QObject::sender()); if (!controller) return; auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras.at(id); + auto& extras = m_instanceExtras[id]; - const bool wasSuccessful = controller->wasSuccessful(); // on success, do... - if (wasSuccessful && controller->instance()->settings()->get("AutoCloseConsole").toBool()) { + if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } @@ -1637,8 +1497,29 @@ void Application::controllerFinished() // quit when there are no more windows. if (shouldExitNow()) { - m_status = wasSuccessful ? Succeeded : Failed; - exit(wasSuccessful ? 0 : 1); + m_status = Status::Succeeded; + exit(0); + } +} + +void Application::controllerFailed(const QString& error) +{ + Q_UNUSED(error); + auto controller = qobject_cast(QObject::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); } } @@ -1651,9 +1532,9 @@ void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); - connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); dlg.exec(); } + emit globalSettingsClosed(); } MainWindow* Application::showMainWindow(bool minimized) @@ -1664,8 +1545,8 @@ MainWindow* Application::showMainWindow(bool minimized) m_mainWindow->activateWindow(); } else { m_mainWindow = new MainWindow(); - m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); - m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); + m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); + m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); if (minimized) { m_mainWindow->showMinimized(); @@ -1681,21 +1562,7 @@ MainWindow* Application::showMainWindow(bool minimized) return m_mainWindow; } -ViewLogWindow* Application::showLogWindow() -{ - if (m_viewLogWindow) { - m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); - m_viewLogWindow->raise(); - m_viewLogWindow->activateWindow(); - } else { - m_viewLogWindow = new ViewLogWindow(); - connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); - m_openWindows++; - } - return m_viewLogWindow; -} - -InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString page) +InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) { if (!instance) return nullptr; @@ -1736,7 +1603,7 @@ InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString void Application::on_windowClose() { m_openWindows--; - auto instWindow = qobject_cast(sender()); + auto instWindow = qobject_cast(QObject::sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; @@ -1745,14 +1612,10 @@ void Application::on_windowClose() extras.controller->setParentWidget(m_mainWindow); } } - auto mainWindow = qobject_cast(sender()); + auto mainWindow = qobject_cast(QObject::sender()); if (mainWindow) { m_mainWindow = nullptr; } - auto logWindow = qobject_cast(sender()); - if (logWindow) { - m_viewLogWindow = nullptr; - } // quit when there are no more windows. if (shouldExitNow()) { exit(0); @@ -1807,22 +1670,22 @@ void Application::updateProxySettings(QString proxyTypeStr, QString addr, int po qDebug() << proxyDesc; } -HttpMetaCache* Application::metacache() +shared_qobject_ptr Application::metacache() { - return m_metacache.get(); + return m_metacache; } -QNetworkAccessManager* Application::network() +shared_qobject_ptr Application::network() { - return m_network.get(); + return m_network; } -Meta::Index* Application::metadataIndex() +shared_qobject_ptr Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } - return m_metadataIndex.get(); + return m_metadataIndex; } void Application::updateCapabilities() @@ -1837,7 +1700,7 @@ void Application::updateCapabilities() if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; - if (!LibraryUtils::findMangoHud().isEmpty()) + if (!MangoHud::getLibraryString().isEmpty()) m_capabilities |= SupportsMangoHud; #endif } @@ -1845,8 +1708,8 @@ void Application::updateCapabilities() void Application::detectLibraries() { #ifdef Q_OS_LINUX - m_detectedGLFWPath = LibraryUtils::find(BuildConfig.GLFW_LIBRARY_NAME); - m_detectedOpenALPath = LibraryUtils::find(BuildConfig.OPENAL_LIBRARY_NAME); + m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); + m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; #endif } @@ -1907,6 +1770,17 @@ QString Application::getUserAgent() return BuildConfig.USER_AGENT; } +QString Application::getUserAgentUncached() +{ + QString uaOverride = m_settings->get("UserAgentOverride").toString(); + if (!uaOverride.isEmpty()) { + uaOverride += " (Uncached)"; + return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); + } + + return BuildConfig.USER_AGENT_UNCACHED; +} + bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, @@ -1952,9 +1826,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:" << file.errorString(); - } + file.open(QIODevice::WriteOnly); }; // create no-migrate file if user doesn't want to migrate @@ -1966,23 +1838,22 @@ bool Application::handleDataMigration(const QString& currentData, if (!currentExists) { // Migrate! - using namespace Filters; - - QList filters; - filters.append(equals(configFile)); - filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before - filters.append(startsWith("logs/")); - filters.append(equals("accounts.json")); - filters.append(startsWith("accounts/")); - filters.append(startsWith("assets/")); - filters.append(startsWith("icons/")); - filters.append(startsWith("instances/")); - filters.append(startsWith("libraries/")); - filters.append(startsWith("mods/")); - filters.append(startsWith("themes/")); + auto matcher = std::make_shared(); + matcher->add(std::make_shared(configFile)); + matcher->add(std::make_shared( + BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + matcher->add(std::make_shared("logs/")); + matcher->add(std::make_shared("accounts.json")); + matcher->add(std::make_shared("accounts/")); + matcher->add(std::make_shared("assets/")); + matcher->add(std::make_shared("icons/")); + matcher->add(std::make_shared("instances/")); + matcher->add(std::make_shared("libraries/")); + matcher->add(std::make_shared("mods/")); + matcher->add(std::make_shared("themes/")); ProgressDialog diag; - DataMigrationTask task(oldData, currentData, any(std::move(filters))); + DataMigrationTask task(oldData, currentData, matcher); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); @@ -2007,7 +1878,7 @@ void Application::triggerUpdateCheck() } } -QUrl Application::normalizeImportUrl(const QString& url) +QUrl Application::normalizeImportUrl(QString const& url) { auto local_file = QFileInfo(url); if (local_file.exists()) { @@ -2048,4 +1919,4 @@ bool Application::checkQSavePath(QString path) } } return false; -} +} \ No newline at end of file diff --git a/launcher/Application.h b/launcher/Application.h index 936e13d71..164ec3a4f 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -37,8 +37,6 @@ #pragma once -#include - #include #include #include @@ -46,16 +44,16 @@ #include #include #include +#include -#include "QObjectPtr.h" +#include -#include "minecraft/auth/MinecraftAccount.h" +#include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; -class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; @@ -74,12 +72,6 @@ class ITheme; class MCEditTool; class ThemeManager; class IconTheme; -class BaseInstance; - -class LogModel; - -struct MinecraftTarget; -class MinecraftAccount; namespace Meta { class Index; @@ -97,6 +89,7 @@ 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 }; @@ -117,29 +110,29 @@ class Application : public QApplication { bool event(QEvent* event) override; - SettingsObject* settings() const { return m_settings.get(); } + std::shared_ptr settings() const { return m_settings; } - qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } + qint64 timeSinceStart() const { return startTime.msecsTo(QDateTime::currentDateTime()); } - QIcon logo(); + QIcon getThemedIcon(const QString& name); ThemeManager* themeManager() { return m_themeManager.get(); } - ExternalUpdater* updater() { return m_updater.get(); } + shared_qobject_ptr updater() { return m_updater; } void triggerUpdateCheck(); - TranslationsModel* translations(); + std::shared_ptr translations(); - JavaInstallList* javalist(); + std::shared_ptr javalist(); - InstanceList* instances() const { return m_instances.get(); } + std::shared_ptr instances() const { return m_instances; } - IconList* icons() const { return m_icons.get(); } + std::shared_ptr icons() const { return m_icons; } MCEditTool* mcedit() const { return m_mcedit.get(); } - AccountList* accounts() const { return m_accounts.get(); } + shared_qobject_ptr accounts() const { return m_accounts; } Status status() const { return m_status; } @@ -147,11 +140,11 @@ class Application : public QApplication { void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); - QNetworkAccessManager* network(); + shared_qobject_ptr network(); - HttpMetaCache* metacache(); + shared_qobject_ptr metacache(); - Meta::Index* metadataIndex(); + shared_qobject_ptr metadataIndex(); void updateCapabilities(); @@ -167,6 +160,7 @@ class Application : public QApplication { QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); + QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } @@ -187,9 +181,8 @@ class Application : public QApplication { */ bool openJsonEditor(const QString& filename); - InstanceWindow* showInstanceWindow(BaseInstance* instance, QString page = QString()); + InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); - ViewLogWindow* showLogWindow(); void updateIsRunning(bool running); bool updatesAreAllowed(); @@ -199,12 +192,12 @@ class Application : public QApplication { bool updaterEnabled(); QString updaterBinaryName(); - QUrl normalizeImportUrl(const QString& url); + QUrl normalizeImportUrl(QString const& url); signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); - void globalSettingsApplied(); + void globalSettingsClosed(); int currentCatChanged(int index); void oauthReplyRecieved(QVariantMap); @@ -214,18 +207,19 @@ class Application : public QApplication { #endif public slots: - bool launch(BaseInstance* instance, - LaunchMode mode = LaunchMode::Normal, - std::shared_ptr targetToJoin = nullptr, - shared_qobject_ptr accountToUse = nullptr, - const QString& offlineName = QString()); - bool kill(BaseInstance* instance); + bool launch(InstancePtr instance, + bool online = true, + bool demo = false, + MinecraftTarget::Ptr targetToJoin = nullptr, + MinecraftAccountPtr accountToUse = nullptr); + bool kill(InstancePtr instance); void closeCurrentWindow(); private slots: void on_windowClose(); void messageReceived(const QByteArray& message); - void controllerFinished(); + void controllerSucceeded(); + void controllerFailed(const QString& error); void setupWizardFinished(int status); private: @@ -242,26 +236,22 @@ class Application : public QApplication { bool shouldExitNow() const; private: - QHash m_qsaveResources; - mutable QMutex m_qsaveResourcesMutex; + QDateTime startTime; - 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::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::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_mcedit; QSet m_features; std::unique_ptr m_themeManager; @@ -278,10 +268,15 @@ 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; - std::unique_ptr controller; + shared_qobject_ptr controller; }; std::map m_instanceExtras; mutable QMutex m_instanceExtrasMutex; @@ -294,9 +289,6 @@ class Application : public QApplication { // main window, if any MainWindow* m_mainWindow = nullptr; - // log window, if any - ViewLogWindow* m_viewLogWindow = nullptr; - // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; @@ -309,17 +301,17 @@ class Application : public QApplication { QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; - 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; - 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 deleted file mode 100644 index 0b1cdb742..000000000 --- a/launcher/AssertHelpers.h +++ /dev/null @@ -1,25 +0,0 @@ -// 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 0080cc516..69cf95e3c 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -42,10 +42,8 @@ #include #include #include +#include -#include "Application.h" -#include "Json.h" -#include "launch/LaunchTask.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -54,26 +52,9 @@ #include "Commandline.h" #include "FileSystem.h" -int getConsoleMaxLines(SettingsObject* settings) +BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() { - auto lineSetting = settings->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - return maxLines; -} - -bool shouldStopOnConsoleOverflow(SettingsObject* settings) -{ - return settings->get("ConsoleOverflowStop").toBool(); -} - -BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : QObject() -{ - m_settings = std::move(settings); + m_settings = settings; m_global_settings = globalSettings; m_rootDir = rootDir; @@ -88,7 +69,6 @@ BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptrregisterSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); - m_settings->registerSetting("shortcuts", QString()); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); @@ -123,13 +103,10 @@ BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptrregisterSetting("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(); @@ -197,35 +174,46 @@ void BaseInstance::copyManagedPack(BaseInstance& other) m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); +} - if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && - m_settings->get("OverrideJavaLocation").toBool()) { - m_settings->set("OverrideJavaLocation", false); - m_settings->set("JavaPath", ""); +int BaseInstance::getConsoleMaxLines() const +{ + auto lineSetting = m_settings->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; } + return maxLines; +} + +bool BaseInstance::shouldStopOnConsoleOverflow() const +{ + return m_settings->get("ConsoleOverflowStop").toBool(); } QStringList BaseInstance::getLinkedInstances() const { - auto setting = m_settings->get("linkedInstances").toString(); - return Json::toStringList(setting); + return m_settings->get("linkedInstances").toStringList(); } void BaseInstance::setLinkedInstances(const QStringList& list) { - m_settings->set("linkedInstances", Json::fromStringList(list)); + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + m_settings->set("linkedInstances", list); } void BaseInstance::addLinkedInstanceId(const QString& id) { - auto linkedInstances = getLinkedInstances(); + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); linkedInstances.append(id); setLinkedInstances(linkedInstances); } bool BaseInstance::removeLinkedInstanceId(const QString& id) { - auto linkedInstances = getLinkedInstances(); + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); int numRemoved = linkedInstances.removeAll(id); setLinkedInstances(linkedInstances); return numRemoved > 0; @@ -233,7 +221,7 @@ bool BaseInstance::removeLinkedInstanceId(const QString& id) bool BaseInstance::isLinkedToInstanceId(const QString& id) const { - auto linkedInstances = getLinkedInstances(); + auto linkedInstances = m_settings->get("linkedInstances").toStringList(); return linkedInstances.contains(id); } @@ -339,11 +327,11 @@ QString BaseInstance::instanceRoot() const return m_rootDir; } -SettingsObject* BaseInstance::settings() +SettingsObjectPtr BaseInstance::settings() { loadSpecificSettings(); - return m_settings.get(); + return m_settings; } bool BaseInstance::canLaunch() const @@ -398,63 +386,6 @@ void BaseInstance::setName(QString val) emit propertiesChanged(this); } -bool BaseInstance::syncInstanceDirName(const QString& newRoot) const -{ - auto oldRoot = instanceRoot(); - return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); -} - -void BaseInstance::registerShortcut(const ShortcutData& data) -{ - auto currentShortcuts = shortcuts(); - currentShortcuts.append(data); - qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; - setShortcuts(currentShortcuts); -} - -void BaseInstance::setShortcuts(const QList& shortcuts) -{ - // FIXME: if no change, do not set. setting involves saving a file. - QJsonArray array; - for (const auto& elem : shortcuts) { - array.append(QJsonObject{ { "name", elem.name }, { "filePath", elem.filePath }, { "target", static_cast(elem.target) } }); - } - - QJsonDocument document; - document.setArray(array); - m_settings->set("shortcuts", QString::fromUtf8(document.toJson(QJsonDocument::Compact))); -} - -QList BaseInstance::shortcuts() const -{ - auto data = m_settings->get("shortcuts").toString().toUtf8(); - QJsonParseError parseError; - auto document = QJsonDocument::fromJson(data, &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isArray()) - return {}; - - QList results; - for (const auto& elem : document.array()) { - if (!elem.isObject()) - continue; - auto dict = elem.toObject(); - if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) - continue; - int value = dict["target"].toInt(-1); - if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) - continue; - - QString shortcutName = dict["name"].toString(); - QString filePath = dict["filePath"].toString(); - if (!QDir(filePath).exists()) { - qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" << filePath; - continue; - } - results.append({ shortcutName, filePath, static_cast(value) }); - } - return results; -} - QString BaseInstance::name() const { return m_settings->get("name").toString(); @@ -471,17 +402,12 @@ QStringList BaseInstance::extraArguments() return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } -LaunchTask* BaseInstance::getLaunchTask() +shared_qobject_ptr BaseInstance::getLaunchTask() { - return m_launchProcess.get(); + return m_launchProcess; } void BaseInstance::updateRuntimeContext() { // NOOP } - -bool BaseInstance::isLegacy() -{ - return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); -} diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 9280d2e1c..2be28d1ec 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -38,9 +38,7 @@ #pragma once #include -#include #include -#include #include #include #include @@ -52,6 +50,7 @@ #include "BaseVersionList.h" #include "MessageLevel.h" #include "minecraft/auth/MinecraftAccount.h" +#include "pathmatcher/IPathMatcher.h" #include "settings/INIFile.h" #include "net/Mode.h" @@ -64,19 +63,8 @@ class Task; class LaunchTask; class BaseInstance; -/// Shortcut saving target representations -enum class ShortcutTarget { Desktop, Applications, Other }; - -/// Shortcut data representation -struct ShortcutData { - QString name; - QString filePath; - ShortcutTarget target = ShortcutTarget::Other; -}; - -/// Console settings -int getConsoleMaxLines(SettingsObject* settings); -bool shouldStopOnConsoleOverflow(SettingsObject* settings); +// pointer for lazy people +using InstancePtr = std::shared_ptr; /*! * \brief Base class for instances. @@ -86,11 +74,11 @@ bool shouldStopOnConsoleOverflow(SettingsObject* 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 { +class BaseInstance : public QObject, public std::enable_shared_from_this { Q_OBJECT protected: /// no-touchy! - BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); + BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); public: /* types */ enum class Status { @@ -100,7 +88,7 @@ class BaseInstance : public QObject { public: /// virtual destructor to make sure the destruction is COMPLETE - virtual ~BaseInstance(); + virtual ~BaseInstance() {} virtual void saveNow() = 0; @@ -138,14 +126,6 @@ class BaseInstance : public QObject { QString name() const; void setName(QString val); - /// Sync name and rename instance dir accordingly; returns true if successful - bool syncInstanceDirName(const QString& newRoot) const; - - /// Register a created shortcut - void registerShortcut(const ShortcutData& data); - QList shortcuts() const; - void setShortcuts(const QList& shortcuts); - /// Value used for instance window titles QString windowTitle() const; @@ -168,6 +148,9 @@ class BaseInstance : public QObject { void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void copyManagedPack(BaseInstance& other); + /// guess log level from a line of game log + virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString& line, MessageLevel::Enum level) { return level; } + virtual QStringList extraArguments(); /// Traits. Normally inside the version, depends on instance implementation. @@ -190,7 +173,7 @@ class BaseInstance : public QObject { * * \return A pointer to this instance's settings object. */ - virtual SettingsObject* settings(); + virtual SettingsObjectPtr settings(); /*! * \brief Loads settings specific to an instance type if they're not already loaded. @@ -201,10 +184,10 @@ class BaseInstance : public QObject { virtual QList createUpdateTask() = 0; /// returns a valid launcher (task container) - virtual LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; + virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) - LaunchTask* getLaunchTask(); + shared_qobject_ptr getLaunchTask(); /*! * Create envrironment variables for running the instance @@ -212,10 +195,15 @@ class BaseInstance : public QObject { virtual QProcessEnvironment createEnvironment() = 0; virtual QProcessEnvironment createLaunchEnvironment() = 0; + /*! + * Returns a matcher that can maps relative paths within the instance to whether they are 'log files' + */ + virtual IPathMatcher::Ptr getLogFileMatcher() = 0; + /*! * Returns the root folder to use for looking up log files */ - virtual QStringList getLogFileSearchPaths() = 0; + virtual QString getLogFileRoot() = 0; virtual QString getStatusbarDescription() = 0; @@ -272,18 +260,19 @@ class BaseInstance : public QObject { Status currentStatus() const; + int getConsoleMaxLines() const; + bool shouldStopOnConsoleOverflow() const; + QStringList getLinkedInstances() const; void setLinkedInstances(const QStringList& list); void addLinkedInstanceId(const QString& id); bool removeLinkedInstanceId(const QString& id); bool isLinkedToInstanceId(const QString& id) const; - bool isLegacy(); - protected: void changeStatus(Status newStatus); - SettingsObject* globalSettings() const { return m_global_settings; } + SettingsObjectPtr globalSettings() const { return m_global_settings.lock(); } bool isSpecificSettingsLoaded() const { return m_specific_settings_loaded; } void setSpecificSettingsLoaded(bool loaded) { m_specific_settings_loaded = loaded; } @@ -294,7 +283,7 @@ class BaseInstance : public QObject { */ void propertiesChanged(BaseInstance* inst); - void launchTaskChanged(LaunchTask*); + void launchTaskChanged(shared_qobject_ptr); void runningStatusChanged(bool running); @@ -307,10 +296,10 @@ class BaseInstance : public QObject { protected: /* data */ QString m_rootDir; - std::unique_ptr m_settings; + SettingsObjectPtr m_settings; // InstanceFlags m_flags; bool m_isRunning = false; - std::unique_ptr m_launchProcess; + shared_qobject_ptr m_launchProcess; QDateTime m_timeStarted; RuntimeContext m_runtimeContext; @@ -320,7 +309,7 @@ class BaseInstance : public QObject { bool m_hasUpdate = false; bool m_hasBrokenVersion = false; - SettingsObject* m_global_settings; + SettingsObjectWeakPtr m_global_settings; bool m_specific_settings_loaded = false; }; diff --git a/launcher/BaseVersion.h b/launcher/BaseVersion.h index e442c60df..2837ff3a9 100644 --- a/launcher/BaseVersion.h +++ b/launcher/BaseVersion.h @@ -24,28 +24,27 @@ */ class BaseVersion { public: - // TODO: delete using Ptr = std::shared_ptr; virtual ~BaseVersion() {} /*! * A string used to identify this version in config files. * This should be unique within the version list or shenanigans will occur. */ - virtual QString descriptor() const = 0; + virtual QString descriptor() = 0; /*! * The name of this version as it is displayed to the user. * For example: "1.5.1" */ - virtual QString name() const = 0; + virtual QString name() = 0; /*! * This should return a string that describes * the kind of version this is (Stable, Beta, Snapshot, whatever) */ virtual QString typeString() const = 0; - virtual bool operator<(BaseVersion& a) const { return name() < a.name(); } - virtual bool operator>(BaseVersion& a) const { return name() > a.name(); } + virtual bool operator<(BaseVersion& a) { return name() < a.name(); } + virtual bool operator>(BaseVersion& a) { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/launcher/BaseVersionList.h b/launcher/BaseVersionList.h index ba546e955..673d13562 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(bool forceReload = false) = 0; + virtual Task::Ptr getLoadTask() = 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 7d4430fd2..0f0949a3a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -21,19 +21,11 @@ set(CORE_SOURCES BaseVersion.h BaseInstance.h BaseInstance.cpp - InstanceDirUpdate.h - InstanceDirUpdate.cpp NullInstance.h MMCZip.h MMCZip.cpp - archive/ArchiveReader.cpp - archive/ArchiveReader.h - archive/ArchiveWriter.cpp - archive/ArchiveWriter.h - archive/ExportToZipTask.cpp - archive/ExportToZipTask.h - archive/ExtractZipTask.cpp - archive/ExtractZipTask.h + Untar.h + Untar.cpp StringUtils.h StringUtils.cpp QVariantUtils.h @@ -62,6 +54,7 @@ set(CORE_SOURCES # String filters Filter.h + Filter.cpp # JSON parsing helpers Json.h @@ -75,6 +68,9 @@ 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 @@ -99,27 +95,32 @@ set(CORE_SOURCES MMCTime.cpp MTPixmapCache.h - - # Assertion helper - AssertHelpers.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) - set(CORE_SOURCES +set(CORE_SOURCES ${CORE_SOURCES} - # LibraryUtils - LibraryUtils.h - LibraryUtils.cpp + # MangoHud + MangoHud.h + MangoHud.cpp ) endif() +set(PATHMATCHER_SOURCES + # Path matchers + pathmatcher/FSTreeMatcher.h + pathmatcher/IPathMatcher.h + pathmatcher/MultiMatcher.h + pathmatcher/RegexpMatcher.h + pathmatcher/SimplePrefixMatcher.h +) + set(NET_SOURCES # network stuffs net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h - net/DummySink.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp @@ -172,8 +173,6 @@ set(LAUNCH_SOURCES launch/LogModel.h launch/TaskStepWrapper.cpp launch/TaskStepWrapper.h - logs/LogParser.cpp - logs/LogParser.h ) # Old update system @@ -244,13 +243,18 @@ 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/gameoptions/GameOptions.h + minecraft/gameoptions/GameOptions.cpp + minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp - minecraft/update/LegacyFMLLibrariesTask.cpp - minecraft/update/LegacyFMLLibrariesTask.h + minecraft/update/FMLLibrariesTask.cpp + minecraft/update/FMLLibrariesTask.h minecraft/update/FoldersTask.cpp minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp @@ -260,10 +264,6 @@ 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 @@ -306,8 +306,6 @@ set(MINECRAFT_SOURCES minecraft/ParseUtils.h minecraft/ProfileUtils.cpp minecraft/ProfileUtils.h - minecraft/ShortcutUtils.cpp - minecraft/ShortcutUtils.h minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h @@ -334,8 +332,6 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp - minecraft/mod/DataPackFolderModel.h - minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h @@ -349,15 +345,17 @@ 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/BasicFolderLoadTask.h + minecraft/mod/tasks/ModFolderLoadTask.h + minecraft/mod/tasks/ModFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.cpp - minecraft/mod/tasks/LocalResourceUpdateTask.h - minecraft/mod/tasks/LocalResourceUpdateTask.cpp + minecraft/mod/tasks/LocalModUpdateTask.h + minecraft/mod/tasks/LocalModUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp + minecraft/mod/tasks/LocalResourcePackParseTask.h + minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -491,11 +489,8 @@ set(META_SOURCES set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp - modplatform/ResourceType.h - modplatform/ResourceType.cpp modplatform/ResourceAPI.h - modplatform/ResourceAPI.cpp modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp @@ -506,6 +501,8 @@ set(API_SOURCES modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp + modplatform/helpers/NetworkResourceAPI.h + modplatform/helpers/NetworkResourceAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h @@ -529,15 +526,12 @@ 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 # Flame + modplatform/flame/FlamePackIndex.cpp + modplatform/flame/FlamePackIndex.h modplatform/flame/FlameModIndex.cpp modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h @@ -555,6 +549,8 @@ set(FLAME_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -592,8 +588,8 @@ set(ATLAUNCHER_SOURCES ) set(LINKEXE_SOURCES - console/WindowsConsole.h - console/WindowsConsole.cpp + WindowsConsole.cpp + WindowsConsole.h filelink/FileLink.h filelink/FileLink.cpp @@ -630,10 +626,6 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp - archive/ArchiveReader.cpp - archive/ArchiveReader.h - archive/ArchiveWriter.cpp - archive/ArchiveWriter.h # Time MMCTime.h @@ -666,14 +658,6 @@ set(PRISMUPDATER_SOURCES ) -if(WIN32) - set(PRISMUPDATER_SOURCES - console/WindowsConsole.h - console/WindowsConsole.cpp - ${PRISMUPDATER_SOURCES} - ) -endif() - ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -761,6 +745,7 @@ endif() set(LOGIC_SOURCES ${CORE_SOURCES} + ${PATHMATCHER_SOURCES} ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} @@ -799,11 +784,6 @@ SET(LAUNCHER_SOURCES ApplicationMessage.cpp SysInfo.h SysInfo.cpp - HardwareInfo.cpp - HardwareInfo.h - - # console utils - console/Console.h # GUI - general utilities DesktopServices.h @@ -817,16 +797,28 @@ 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 + ../${Launcher_Branding_LogoQRC} + # Icons icons/MMCIcon.h icons/MMCIcon.cpp icons/IconList.h icons/IconList.cpp - # log utils - logs/AnonymizeLog.cpp - logs/AnonymizeLog.h - # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp @@ -834,10 +826,6 @@ SET(LAUNCHER_SOURCES ui/MainWindow.cpp ui/InstanceWindow.h 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 @@ -855,6 +843,7 @@ SET(LAUNCHER_SOURCES ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h + ui/setupwizard/ThemeWizardPage.cpp ui/setupwizard/ThemeWizardPage.h ui/setupwizard/AutoJavaWizardPage.cpp ui/setupwizard/AutoJavaWizardPage.h @@ -882,11 +871,8 @@ SET(LAUNCHER_SOURCES ui/themes/ThemeManager.h ui/themes/CatPack.cpp ui/themes/CatPack.h - ui/themes/CatPainter.cpp - ui/themes/CatPainter.h # Processes - LaunchMode.h LaunchController.h LaunchController.cpp @@ -905,12 +891,12 @@ SET(LAUNCHER_SOURCES # GUI - instance pages ui/pages/instance/ExternalResourcesPage.cpp ui/pages/instance/ExternalResourcesPage.h + ui/pages/instance/GameOptionsPage.cpp + ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h - ui/pages/instance/DataPackPage.h - ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h @@ -923,6 +909,7 @@ SET(LAUNCHER_SOURCES ui/pages/instance/NotesPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h + ui/pages/instance/InstanceSettingsPage.cpp ui/pages/instance/InstanceSettingsPage.h ui/pages/instance/ScreenshotsPage.cpp ui/pages/instance/ScreenshotsPage.h @@ -932,26 +919,24 @@ SET(LAUNCHER_SOURCES ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h - ui/pages/instance/McClient.cpp - ui/pages/instance/McClient.h - ui/pages/instance/McResolver.cpp - ui/pages/instance/McResolver.h - ui/pages/instance/ServerPingTask.cpp - ui/pages/instance/ServerPingTask.h # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h + ui/pages/global/CustomCommandsPage.cpp + ui/pages/global/CustomCommandsPage.h + ui/pages/global/EnvironmentVariablesPage.cpp + ui/pages/global/EnvironmentVariablesPage.h ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.h ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.h ui/pages/global/LanguagePage.cpp ui/pages/global/LanguagePage.h + ui/pages/global/MinecraftPage.cpp ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h - ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h ui/pages/global/APIPage.cpp @@ -981,8 +966,6 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp - ui/pages/modplatform/DataPackPage.cpp - ui/pages/modplatform/DataPackModel.cpp ui/pages/modplatform/ModpackProviderBasePage.h @@ -997,13 +980,6 @@ 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 @@ -1039,6 +1015,8 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/OptionalModDialog.cpp ui/pages/modplatform/OptionalModDialog.h + ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp + ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -1051,10 +1029,10 @@ SET(LAUNCHER_SOURCES ui/dialogs/ProfileSetupDialog.h ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.h - ui/dialogs/CreateShortcutDialog.cpp - ui/dialogs/CreateShortcutDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h + ui/dialogs/EditAccountDialog.cpp + ui/dialogs/EditAccountDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportPackDialog.cpp @@ -1067,8 +1045,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/OfflineLoginDialog.cpp + ui/dialogs/OfflineLoginDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp @@ -1091,23 +1069,14 @@ SET(LAUNCHER_SOURCES ui/dialogs/BlockedModsDialog.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp - ui/dialogs/ResourceUpdateDialog.cpp - ui/dialogs/ResourceUpdateDialog.h + ui/dialogs/ModUpdateDialog.cpp + ui/dialogs/ModUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h - ui/dialogs/ChooseOfflineNameDialog.cpp - ui/dialogs/ChooseOfflineNameDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h - ui/dialogs/skins/draw/SkinOpenGLWindow.h - ui/dialogs/skins/draw/SkinOpenGLWindow.cpp - ui/dialogs/skins/draw/Scene.h - ui/dialogs/skins/draw/Scene.cpp - ui/dialogs/skins/draw/BoxGeometry.h - ui/dialogs/skins/draw/BoxGeometry.cpp - # GUI - widgets ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h @@ -1117,14 +1086,20 @@ SET(LAUNCHER_SOURCES ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h + ui/widgets/DropLabel.cpp + ui/widgets/DropLabel.h + ui/widgets/FocusLineEdit.cpp + ui/widgets/FocusLineEdit.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h - ui/widgets/JavaWizardWidget.cpp - ui/widgets/JavaWizardWidget.h + ui/widgets/JavaSettingsWidget.cpp + ui/widgets/JavaSettingsWidget.h ui/widgets/LabeledToolButton.cpp ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp ui/widgets/LanguageSelectionWidget.h + ui/widgets/LineSeparator.cpp + ui/widgets/LineSeparator.h ui/widgets/LogView.cpp ui/widgets/LogView.h ui/widgets/InfoFrame.cpp @@ -1152,12 +1127,8 @@ SET(LAUNCHER_SOURCES ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp - ui/widgets/AppearanceWidget.h - ui/widgets/AppearanceWidget.cpp - ui/widgets/MinecraftSettingsWidget.h - ui/widgets/MinecraftSettingsWidget.cpp - ui/widgets/JavaSettingsWidget.h - ui/widgets/JavaSettingsWidget.cpp + ui/widgets/ThemeCustomizationWidget.h + ui/widgets/ThemeCustomizationWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp @@ -1173,15 +1144,8 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) -if (APPLE) - set(LAUNCHER_SOURCES - ${LAUNCHER_SOURCES} - ui/themes/ThemeManager.mm - ) -endif() - if (NOT Apple) - set(LAUNCHER_SOURCES +set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/dialogs/UpdateAvailableDialog.h @@ -1189,21 +1153,86 @@ if (NOT Apple) ) endif() -if (APPLE) +if(WIN32) set(LAUNCHER_SOURCES + WindowsConsole.cpp + WindowsConsole.h ${LAUNCHER_SOURCES} - - macsandbox/SecurityBookmarkFileAccess.h - macsandbox/SecurityBookmarkFileAccess.mm ) endif() -if(WIN32) - set(LAUNCHER_SOURCES - console/WindowsConsole.h - console/WindowsConsole.cpp - ${LAUNCHER_SOURCES} - ) +qt_wrap_ui(LAUNCHER_UI + ui/MainWindow.ui + ui/setupwizard/PasteWizardPage.ui + ui/setupwizard/AutoJavaWizardPage.ui + ui/setupwizard/LoginWizardPage.ui + ui/setupwizard/ThemeWizardPage.ui + ui/pages/global/AccountListPage.ui + ui/pages/global/JavaPage.ui + ui/pages/global/LauncherPage.ui + ui/pages/global/APIPage.ui + ui/pages/global/ProxyPage.ui + ui/pages/global/MinecraftPage.ui + ui/pages/global/ExternalToolsPage.ui + ui/pages/instance/ExternalResourcesPage.ui + ui/pages/instance/NotesPage.ui + ui/pages/instance/LogPage.ui + ui/pages/instance/ServersPage.ui + ui/pages/instance/GameOptionsPage.ui + ui/pages/instance/OtherLogsPage.ui + ui/pages/instance/InstanceSettingsPage.ui + ui/pages/instance/VersionPage.ui + ui/pages/instance/ManagedPackPage.ui + ui/pages/instance/WorldListPage.ui + 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/InstanceCardWidget.ui + ui/widgets/CustomCommands.ui + ui/widgets/EnvironmentVariables.ui + ui/widgets/InfoFrame.ui + ui/widgets/ModFilterWidget.ui + ui/widgets/SubTaskProgressBar.ui + ui/widgets/ThemeCustomizationWidget.ui + ui/dialogs/CopyInstanceDialog.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/OfflineLoginDialog.ui + ui/dialogs/AboutDialog.ui + ui/dialogs/EditAccountDialog.ui + ui/dialogs/ReviewMessageBox.ui + ui/dialogs/ScrollMessageBox.ui + ui/dialogs/BlockedModsDialog.ui + ui/dialogs/ChooseProviderDialog.ui + + ui/dialogs/skins/SkinManageDialog.ui +) + +qt_wrap_ui(PRISM_UPDATE_UI + ui/dialogs/UpdateAvailableDialog.ui +) + +if (NOT Apple) + set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) endif() qt_add_resources(LAUNCHER_RESOURCES @@ -1218,10 +1247,14 @@ 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}" + ../${Launcher_Branding_LogoQRC} +) + +qt_wrap_ui(PRISMUPDATER_UI + updater/prismupdater/SelectReleaseDialog.ui + ui/widgets/SubTaskProgressBar.ui + ui/dialogs/ProgressDialog.ui ) ######## Windows resource files ######## @@ -1229,56 +1262,29 @@ if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() -######## Precompiled Headers ########### - -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 ######## +include(CompilerWarnings) # Add executable -add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_RESOURCES}) +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}") target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) - -if(${Launcher_USE_PCH}) - target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) -endif() - target_link_libraries(Launcher_logic + systeminfo Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} + tomlplusplus::tomlplusplus qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets + ghcFilesystem::ghc_filesystem ) -if(TARGET PkgConfig::libqrencode) - target_link_libraries(Launcher_logic PkgConfig::libqrencode) -else() - target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR}) - target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES}) -endif() - -if(TARGET PkgConfig::tomlplusplus) - target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) -else() - target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) -endif() -if(TARGET PkgConfig::libarchive) - target_link_libraries(Launcher_logic PkgConfig::libarchive) -else() - target_link_libraries(Launcher_logic LibArchive::LibArchive) -endif() - -if (CMAKE_SYSTEM_NAME STREQUAL "Linux") +if (UNIX AND NOT CYGWIN AND NOT APPLE) target_link_libraries(Launcher_logic gamemode ) @@ -1292,29 +1298,24 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth - Qt${QT_VERSION_MAJOR}::OpenGL - ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic + QuaZip::QuaZip cmark::cmark LocalPeer Launcher_rainbow ) -if (TARGET ${Launcher_QT_DBUS}) - add_compile_definitions(WITH_QTDBUS) -endif() - if(APPLE) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") if(Launcher_ENABLE_UPDATER) - file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) - file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) - find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") - add_compile_definitions(SPARKLE_ENABLED) + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + add_compile_definitions(SPARKLE_ENABLED) endif() target_link_libraries(Launcher_logic @@ -1324,16 +1325,13 @@ if(APPLE) "-framework ApplicationServices" ) if(Launcher_ENABLE_UPDATER) - target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) endif() endif() +target_link_libraries(Launcher_logic) + add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) - -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) @@ -1349,50 +1347,33 @@ if(DEFINED Launcher_APP_BINARY_DEFS) endif() install(TARGETS ${Launcher_Name} - RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) -# Deploy PDBs -if(CMAKE_CXX_LINKER_SUPPORTS_PDB) - install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) -endif() - if(Launcher_BUILD_UPDATER) # Updater - add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES}) + add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - - if(${Launcher_USE_PCH}) - target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) - endif() - target_link_libraries(prism_updater_logic + QuaZip::QuaZip ${ZLIB_LIBRARIES} + systeminfo BuildConfig + ghcFilesystem::ghc_filesystem Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network ${Launcher_QT_LIBS} cmark::cmark ) - if(TARGET PkgConfig::libarchive) - target_link_libraries(prism_updater_logic PkgConfig::libarchive) - else() - target_link_libraries(prism_updater_logic LibArchive::LibArchive) - endif() add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) - if(${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") endif() @@ -1406,25 +1387,21 @@ if(Launcher_BUILD_UPDATER) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) - - # Deploy PDBs - if(CMAKE_CXX_LINKER_SUPPORTS_PDB) - install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) - endif() 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}) - - if(${Launcher_USE_PCH}) - target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) - endif() - target_link_libraries(filelink_logic + systeminfo BuildConfig + ghcFilesystem::ghc_filesystem Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network @@ -1433,19 +1410,9 @@ 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) - 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 - # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests - if(MSVC) - set_property(TARGET "${Launcher_Name}_filelink" PROPERTY LINK_FLAGS "/MANIFESTUAC:level='requireAdministrator'") - endif() - target_link_libraries("${Launcher_Name}_filelink" filelink_logic) if(DEFINED Launcher_APP_BINARY_NAME) @@ -1461,11 +1428,6 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) - - # Deploy PDBs - if(CMAKE_CXX_LINKER_SUPPORTS_PDB) - install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) - endif() endif() if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) @@ -1475,142 +1437,187 @@ 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. +# Bundle utilities are used to complete the portable packages - 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. -if(WIN32 OR (UNIX AND APPLE)) - if(WIN32) - set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") - endif() - - qt_generate_deploy_script( - TARGET ${Launcher_Name} - OUTPUT_SCRIPT QT_DEPLOY_SCRIPT - CONTENT " - qt_deploy_runtime_dependencies( - EXECUTABLE ${BINARY_DEST_DIR}/$ - BIN_DIR ${BINARY_DEST_DIR} - LIBEXEC_DIR ${LIBRARY_DEST_DIR} - LIB_DIR ${LIBRARY_DEST_DIR} - PLUGINS_DIR ${PLUGIN_DEST_DIR} - NO_OVERWRITE - NO_TRANSLATIONS - NO_COMPILER_RUNTIME - DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} - )" - ) - - # Bundle our linked dependencies - install( - RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET - COMPONENT bundle - DIRECTORIES - ${CMAKE_SYSTEM_LIBRARY_PATH} - ${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" - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} - RUNTIME DESTINATION ${BINARY_DEST_DIR} - FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} - ) - # Deploy Qt plugins - install( - 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() - +if(INSTALL_BUNDLE STREQUAL "full") # Add qt.conf - this makes Qt stop looking for things outside the bundle install( CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" - COMPONENT bundle + COMPONENT Runtime ) - # Add qtlogging.ini as a config file + # add qtlogging.ini as a config file install( FILES "qtlogging.ini" DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} - COMPONENT bundle + COMPONENT Runtime ) -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} + # Bundle plugins + # Image formats + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng" EXCLUDE ) -else() - message(WARNING "Unable to find `clang-format`. Not creating custom target") + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Icon engines + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Platform plugins + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + REGEX "[^2]d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + # TLS plugins (Qt 6 only) + if(EXISTS "${QT_PLUGINS_DIR}/tls") + install( + DIRECTORY "${QT_PLUGINS_DIR}/tls" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + PATTERN "*qcertonlybackend*" EXCLUDE + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/tls" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + PATTERN "*qcertonlybackend*" EXCLUDE + ) + endif() + # Wayland support + if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration") + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" + CONFIGURATIONS Debug RelWithDebInfo "" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + install( + DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" + CONFIGURATIONS Release MinSizeRel + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "dd\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" + @ONLY + ) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) endif() diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index cab22089e..4ffa26fd4 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -12,10 +12,10 @@ #include -DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { - m_copy.matcher(m_pathMatcher).whitelist(true); + m_copy.matcher(m_pathMatcher.get()).whitelist(true); } void DataMigrationTask::executeTask() @@ -24,7 +24,7 @@ void DataMigrationTask::executeTask() // 1. Scan // Check how many files we gotta copy - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { return m_copy(true); // dry run to collect amount of files }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); @@ -37,7 +37,11 @@ void DataMigrationTask::dryRunFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { +#else + if (!m_copyFuture.result()) { +#endif emitFailed(tr("Failed to scan source path.")); return; } @@ -53,7 +57,7 @@ void DataMigrationTask::dryRunFinished() setProgress(m_copy.totalCopied(), m_toCopy); setStatus(tr("Copying %1…").arg(shortenedName)); }); - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { return m_copy(false); // actually copy now }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); @@ -63,7 +67,7 @@ void DataMigrationTask::dryRunFinished() void DataMigrationTask::dryRunAborted() { - emitAborted(); + emitFailed(tr("Aborted")); } void DataMigrationTask::copyFinished() @@ -71,7 +75,11 @@ void DataMigrationTask::copyFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { +#else + if (!m_copyFuture.result()) { +#endif emitFailed(tr("Some paths could not be copied!")); return; } @@ -81,5 +89,5 @@ void DataMigrationTask::copyFinished() void DataMigrationTask::copyAborted() { - emitAborted(); + emitFailed(tr("Aborted")); } diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h index 9a2b0adb8..fc613cd5e 100644 --- a/launcher/DataMigrationTask.h +++ b/launcher/DataMigrationTask.h @@ -5,7 +5,7 @@ #pragma once #include "FileSystem.h" -#include "Filter.h" +#include "pathmatcher/IPathMatcher.h" #include "tasks/Task.h" #include @@ -18,7 +18,7 @@ class DataMigrationTask : public Task { Q_OBJECT public: - explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); ~DataMigrationTask() override = default; protected: @@ -33,7 +33,7 @@ class DataMigrationTask : public Task { private: const QString& m_sourcePath; const QString& m_targetPath; - const Filter m_pathMatcher; + const IPathMatcher::Ptr m_pathMatcher; FS::copy m_copy; int m_toCopy = 0; diff --git a/launcher/DefaultVariable.h b/launcher/DefaultVariable.h new file mode 100644 index 000000000..b082091c7 --- /dev/null +++ b/launcher/DefaultVariable.h @@ -0,0 +1,23 @@ +#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 1dbab27ba..f2b6f4425 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 7799b7879..208534044 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 445c2a881..df06c3c75 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -40,11 +40,12 @@ #include #include #include +#include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" -FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {} +FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {} // NOTE: Sadly, we have to do sorting ourselves. bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const { @@ -103,10 +104,10 @@ QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const if (index.column() == 0 && role == Qt::CheckStateRole) { QFileSystemModel* fsm = qobject_cast(sourceModel()); auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto cover = m_blocked.cover(blockedPath); + auto cover = blocked.cover(blockedPath); if (!cover.isNull()) { return QVariant(Qt::Unchecked); - } else if (m_blocked.exists(blockedPath)) { + } else if (blocked.exists(blockedPath)) { return QVariant(Qt::PartiallyChecked); } else { return QVariant(Qt::Checked); @@ -129,7 +130,7 @@ bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, i QString FileIgnoreProxy::relPath(const QString& path) const { - return QDir(m_root).relativeFilePath(path); + return QDir(root).relativeFilePath(path); } bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) @@ -145,18 +146,18 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) bool changed = false; if (state == Qt::Unchecked) { // blocking a path - auto& node = m_blocked.insert(blockedPath); + auto& node = blocked.insert(blockedPath); // get rid of all blocked nodes below node.clear(); changed = true; } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { - if (!m_blocked.remove(blockedPath)) { - auto cover = m_blocked.cover(blockedPath); + if (!blocked.remove(blockedPath)) { + auto cover = blocked.cover(blockedPath); qDebug() << "Blocked by cover" << cover; // uncover - m_blocked.remove(cover); + blocked.remove(cover); // block all contents, except for any cover - QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover)); + QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover)); QModelIndex doing = rootIndex; int row = 0; QStack todo; @@ -178,7 +179,7 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) todo.push(node); } else { // or just block this one. - m_blocked.insert(relpath); + blocked.insert(relpath); } row++; } @@ -228,7 +229,7 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) return false; } auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto found = m_blocked.find(blockedPath); + auto found = blocked.find(blockedPath); if (found) { return !found->leaf(); } @@ -238,8 +239,8 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) void FileIgnoreProxy::setBlockedPaths(QStringList paths) { beginResetModel(); - m_blocked.clear(); - m_blocked.insert(paths); + blocked.clear(); + blocked.insert(paths); endResetModel(); } @@ -266,45 +267,10 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { - 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; + return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); } -bool FileIgnoreProxy::filterFile(const QFileInfo& file) const +bool FileIgnoreProxy::filterFile(const QString& fileName) const { - return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); -} - -void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) -{ - QFile ignoreFile(fileName); - if (!ignoreFile.open(QIODevice::ReadOnly)) { - return; - } - auto ignoreData = ignoreFile.readAll(); - auto string = QString::fromUtf8(ignoreData); - setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); -} - -void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) -{ - auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8(); - try { - FS::write(fileName, ignoreData); - } catch (const Exception& e) { - qWarning() << e.cause(); - } + return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName)); } diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index 0f149ecb6..e01a2651e 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -61,20 +61,15 @@ class FileIgnoreProxy : public QSortFilterProxyModel { void setBlockedPaths(QStringList paths); - inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; } - inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; } + inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } + inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } // 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; } - bool filterFile(const QFileInfo& fileName) const; - - void loadBlockedPathsFromFile(const QString& fileName); - - void saveBlockedPathsToFile(const QString& fileName); + bool filterFile(const QString& fileName) const; protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; @@ -83,9 +78,8 @@ class FileIgnoreProxy : public QSortFilterProxyModel { bool ignoreFile(QFileInfo file) const; private: - const QString m_root; - SeparatorPrefixTree<'/'> m_blocked; + const QString root; + SeparatorPrefixTree<'/'> blocked; QStringList m_ignoreFiles; - QStringList m_ignoreFilesSuffixes; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index ef56e3e65..512de28c2 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -36,7 +36,6 @@ */ #include "FileSystem.h" -#include #include #include "BuildConfig.h" @@ -60,8 +59,10 @@ #if defined Q_OS_WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN +#include #include #include +#include #include #include #include @@ -76,8 +77,24 @@ #include #endif +// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header + +#ifdef __APPLE__ +#include // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ + +#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) +#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) +#define GHC_USE_STD_FS #include namespace fs = std::filesystem; +#endif // MacOS min version check +#endif // Other OSes version check + +#ifndef GHC_USE_STD_FS +#include +namespace fs = ghc::filesystem; +#endif // clone #if defined(Q_OS_LINUX) @@ -106,10 +123,6 @@ namespace fs = std::filesystem; #if defined(__MINGW32__) -// Avoid re-defining structs retroactively added to MinGW -// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729 -#if __MINGW64_VERSION_MAJOR < 13 - struct _DUPLICATE_EXTENTS_DATA { HANDLE FileHandle; LARGE_INTEGER SourceFileOffset; @@ -119,7 +132,6 @@ struct _DUPLICATE_EXTENTS_DATA { using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; -#endif struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 @@ -283,9 +295,6 @@ 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; } @@ -332,8 +341,8 @@ bool copy::operator()(const QString& offset, bool dryRun) opt |= copy_opts::overwrite_existing; // Function that'll do the actual copying - auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) + auto copy_file = [&](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); @@ -419,8 +428,8 @@ void create_link::make_link_list(const QString& offset) m_recursive = true; // Function that'll do the actual linking - auto link_file = [this, dst](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) { + auto link_file = [&](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; } @@ -436,7 +445,7 @@ void create_link::make_link_list(const QString& offset) link_file(src, ""); } else { if (m_debug) - qDebug().nospace() << "linking recursively: " << src << " to " << dst << ", max_depth: " << m_max_depth; + qDebug() << "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); @@ -514,7 +523,7 @@ void create_link::runPrivileged(const QString& offset) QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); - connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() { + connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() { qDebug() << "Client connected, sending out pairs"; // construct block of data to send QByteArray block; @@ -596,7 +605,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, [&]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); @@ -684,34 +693,11 @@ 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) { +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + return false; +#else // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; @@ -720,6 +706,7 @@ bool trash(QString path, QString* pathInTrash) return false; #endif return QFile::moveToTrash(path, pathInTrash); +#endif } QString PathCombine(const QString& path1, const QString& path2) @@ -753,7 +740,11 @@ int pathDepth(const QString& path) QFileInfo info(path); +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); +#else auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); +#endif int numParts = parts.length(); numParts -= parts.count("."); @@ -773,7 +764,11 @@ QString pathTruncate(const QString& path, int depth) return pathTruncate(trunc, depth); } +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts); +#else auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); +#endif if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); @@ -823,33 +818,68 @@ QString NormalizePath(QString path) } } -namespace { -const QString g_badChars = "<>:\"|?*\r\n!"; -QString removeChars(QString source, QChar replace, const QString& extraChars = "") -{ - auto badChars = g_badChars; - if (!extraChars.isEmpty()) { - badChars += extraChars; - } +static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; +static const QString BAD_NTFS_CHARS = "<>:\"|?*"; +static const QString BAD_HFS_CHARS = ":"; - for (auto& c : source) { - if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) { - c = replace; - } - } - - return source; -} -} // namespace +static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { - return removeChars(std::move(string), replaceWith, "\\/"); + for (int i = 0; i < string.length(); i++) + if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) + string[i] = replaceWith; + return string; } -QString RemoveInvalidPathChars(QString string, QChar replaceWith) +QString RemoveInvalidPathChars(QString path, QChar replaceWith) { - return removeChars(std::move(string), replaceWith); + QString invalidChars; +#ifdef Q_OS_WIN + invalidChars = BAD_WIN_CHARS; +#endif + + // the null character is ignored in this check as it was not a problem until now + switch (statFS(path).fsType) { + case FilesystemType::FAT: // similar to NTFS + /* fallthrough */ + case FilesystemType::NTFS: + /* fallthrough */ + case FilesystemType::REFS: // similar to NTFS(should be available only on windows) + invalidChars += BAD_NTFS_CHARS; + break; + // case FilesystemType::EXT: + // case FilesystemType::EXT_2_OLD: + // case FilesystemType::EXT_2_3_4: + // case FilesystemType::XFS: + // case FilesystemType::BTRFS: + // case FilesystemType::NFS: + // case FilesystemType::ZFS: + case FilesystemType::APFS: + /* fallthrough */ + case FilesystemType::HFS: + /* fallthrough */ + case FilesystemType::HFSPLUS: + /* fallthrough */ + case FilesystemType::HFSX: + invalidChars += BAD_HFS_CHARS; + break; + // case FilesystemType::FUSEBLK: + // case FilesystemType::F2FS: + // case FilesystemType::UNKNOWN: + default: + break; + } + + if (invalidChars.size() != 0) { + for (int i = 0; i < path.length(); i++) { + if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { + path[i] = replaceWith; + } + } + } + + return path; } QString DirNameFromString(QString string, QString inDir) @@ -885,70 +915,48 @@ QString getDesktopDir() return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } -QString getApplicationsDir() -{ - return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); -} - -QString quoteArgs(const QStringList& args, const QString& wrap, const QString& escapeChar, bool wrapOnlyIfNeeded = false) -{ - QString result; - - auto size = args.size(); - for (int i = 0; i < size; ++i) { - QString arg = args[i]; - arg.replace(wrap, escapeChar); - - bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); - - if (needsWrapping) - result += wrap + arg + wrap; - else - result += arg; - - if (i < size - 1) - result += ' '; - } - - return result; -} - // Cross-platform Shortcut creation -QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) +bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; - return QString(); + return false; } #if defined(Q_OS_MACOS) - QDir application = destination + ".app/"; + // Create the Application + QDir applicationDirectory = + QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/"; + + if (!applicationDirectory.mkpath(".")) { + qWarning() << "Couldn't create application directory"; + return false; + } + + QDir application = applicationDirectory.path() + "/" + name + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; - return QString(); + return false; } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; - return QString(); + return false; } QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; - QFile info(content.path() + "/Info.plist"); + QFile info = content.path() + "/Info.plist"; if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; - return QString(); - } - if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Failed to open file" << info.fileName() << "for writing:" << info.errorString(); - return QString(); + return false; } + info.open(QIODevice::WriteOnly | QIODevice::Text); QFile(icon).rename(resources.path() + "/Icon.icns"); @@ -956,13 +964,12 @@ QString createShortcut(QString destination, QString target, QStringList args, QS QString exec = binaryDir.path() + "/Run.command"; QFile f(exec); - if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); - return QString(); - } + f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); - auto argstring = quoteArgs(args, "\"", "\\\""); + QString argstring; + if (!args.empty()) + argstring = " \"" + args.join("\" \"") + "\""; stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; @@ -996,23 +1003,22 @@ QString createShortcut(QString destination, QString target, QStringList args, QS "\n" ""; - return application.path(); + return true; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated destination += ".desktop"; QFile f(destination); - if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); - return QString(); - } + f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); - auto argstring = quoteArgs(args, "'", "'\\''"); + QString argstring; + if (!args.empty()) + argstring = " '" + args.join("' '") + "'"; stream << "[Desktop Entry]" << "\n"; stream << "Type=Application" << "\n"; stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; - stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; + stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; @@ -1023,38 +1029,51 @@ QString createShortcut(QString destination, QString target, QStringList args, QS f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); - return destination; + return true; #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; - return QString(); + return false; } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; - return QString(); + return false; } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; - return QString(); + return false; } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; - return QString(); + return false; + } + + QString argStr; + int argCount = args.count(); + for (int i = 0; i < argCount; i++) { + if (args[i].contains(' ')) { + argStr.append('"').append(args[i]).append('"'); + } else { + argStr.append(args[i]); + } + + if (i < argCount - 1) { + argStr.append(" "); + } } - auto argStr = quoteArgs(args, "\"", "\\\"", true); if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; - return QString(); + return false; } HRESULT hres; @@ -1063,7 +1082,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; - return QString(); + return false; } WCHAR wsz[MAX_PATH]; @@ -1101,28 +1120,26 @@ 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 CoUninitialize(); - if (SUCCEEDED(hres)) - return destination; - return QString(); + return SUCCEEDED(hres); #else qWarning("Desktop Shortcuts not supported on your platform!"); - return QString(); + return false; #endif } @@ -1278,8 +1295,8 @@ bool clone::operator()(const QString& offset, bool dryRun) std::error_code err; // Function that'll do the actual cloneing - auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) + auto cloneFile = [&](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); @@ -1400,14 +1417,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; } @@ -1703,14 +1720,4 @@ QString getUniqueResourceName(const QString& filePath) return newFileName; } -bool removeFiles(QStringList listFile) -{ - bool ret = true; - // For each file - for (int i = 0; i < listFile.count(); i++) { - // Remove - ret = ret && QFile::remove(listFile.at(i)); - } - return ret; -} } // namespace FS diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 6d9b01178..c5beef7bd 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -38,7 +38,7 @@ #pragma once #include "Exception.h" -#include "Filter.h" +#include "pathmatcher/IPathMatcher.h" #include @@ -115,9 +115,9 @@ class copy : public QObject { m_followSymlinks = follow; return *this; } - copy& matcher(Filter filter) + copy& matcher(const IPathMatcher* filter) { - m_matcher = std::move(filter); + m_matcher = filter; return *this; } copy& whitelist(bool whitelist) @@ -147,7 +147,7 @@ class copy : public QObject { private: bool m_followSymlinks = true; - Filter m_matcher = nullptr; + const IPathMatcher* m_matcher = nullptr; bool m_whitelist = false; bool m_overwrite = false; QDir m_src; @@ -209,9 +209,9 @@ class create_link : public QObject { m_useHardLinks = useHard; return *this; } - create_link& matcher(Filter filter) + create_link& matcher(const IPathMatcher* filter) { - m_matcher = std::move(filter); + m_matcher = filter; return *this; } create_link& whitelist(bool whitelist) @@ -260,7 +260,7 @@ class create_link : public QObject { private: bool m_useHardLinks = false; - Filter m_matcher = nullptr; + const IPathMatcher* m_matcher = nullptr; bool m_whitelist = false; bool m_recursive = true; @@ -291,15 +291,6 @@ 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); - /** * Trash a folder / file */ @@ -362,18 +353,14 @@ bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop QString getDesktopDir(); -// Get the Directory representing the User's Applications directory -QString getApplicationsDir(); - // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides bool overrideFolder(QString overwritten_path, QString override_path); /** * Creates a shortcut to the specified target file at the specified destination path. - * Returns null QString if creation failed; otherwise returns the path to the created shortcut. */ -QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); +bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); enum class FilesystemType { FAT, @@ -501,9 +488,9 @@ class clone : public QObject { m_src.setPath(src); m_dst.setPath(dst); } - clone& matcher(Filter filter) + clone& matcher(const IPathMatcher* filter) { - m_matcher = std::move(filter); + m_matcher = filter; return *this; } clone& whitelist(bool whitelist) @@ -527,7 +514,7 @@ class clone : public QObject { bool operator()(const QString& offset, bool dryRun = false); private: - Filter m_matcher = nullptr; + const IPathMatcher* m_matcher = nullptr; bool m_whitelist = false; QDir m_src; QDir m_dst; diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp new file mode 100644 index 000000000..adeb2209e --- /dev/null +++ b/launcher/Filter.cpp @@ -0,0 +1,37 @@ +#include "Filter.h" + +ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern) {} +bool ContainsFilter::accepts(const QString& value) +{ + return value.contains(pattern); +} + +ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern) {} +bool ExactFilter::accepts(const QString& value) +{ + return value == pattern; +} + +ExactIfPresentFilter::ExactIfPresentFilter(const QString& pattern) : pattern(pattern) {} +bool ExactIfPresentFilter::accepts(const QString& value) +{ + return value.isEmpty() || value == pattern; +} + +RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) +{ + pattern.setPattern(regexp); + pattern.optimize(); +} +bool RegexpFilter::accepts(const QString& value) +{ + auto match = pattern.match(value); + bool matched = match.hasMatch(); + return invert ? (!matched) : (matched); +} + +ExactListFilter::ExactListFilter(const QStringList& pattern) : m_pattern(pattern) {} +bool ExactListFilter::accepts(const QString& value) +{ + return m_pattern.isEmpty() || m_pattern.contains(value); +} \ No newline at end of file diff --git a/launcher/Filter.h b/launcher/Filter.h index 317f5b067..ae835e724 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -3,52 +3,59 @@ #include #include -using Filter = std::function; +class Filter { + public: + virtual ~Filter() = default; + virtual bool accepts(const QString& value) = 0; +}; -namespace Filters { -inline Filter inverse(Filter filter) -{ - return [filter = std::move(filter)](const QString& src) { return !filter(src); }; -} +class ContainsFilter : public Filter { + public: + ContainsFilter(const QString& pattern); + virtual ~ContainsFilter() = default; + bool accepts(const QString& value) override; -inline Filter any(QList filters) -{ - return [filters = std::move(filters)](const QString& src) { - for (auto& filter : filters) - if (filter(src)) - return true; + private: + QString pattern; +}; - return false; - }; -} +class ExactFilter : public Filter { + public: + ExactFilter(const QString& pattern); + virtual ~ExactFilter() = default; + bool accepts(const QString& value) override; -inline Filter equals(QString pattern) -{ - return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; -} + private: + QString pattern; +}; -inline Filter equalsAny(QStringList patterns = {}) -{ - return [patterns = std::move(patterns)](const QString& src) { return patterns.isEmpty() || patterns.contains(src); }; -} +class ExactIfPresentFilter : public Filter { + public: + ExactIfPresentFilter(const QString& pattern); + virtual ~ExactIfPresentFilter() override = default; + bool accepts(const QString& value) override; -inline Filter equalsOrEmpty(QString pattern) -{ - return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; -} + private: + QString pattern; +}; -inline Filter contains(QString pattern) -{ - return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; -} +class RegexpFilter : public Filter { + public: + RegexpFilter(const QString& regexp, bool invert); + virtual ~RegexpFilter() = default; + bool accepts(const QString& value) override; -inline Filter startsWith(QString pattern) -{ - return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; -} + private: + QRegularExpression pattern; + bool invert = false; +}; -inline Filter regexp(QRegularExpression pattern) -{ - return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; -} -} // namespace Filters +class ExactListFilter : public Filter { + public: + ExactListFilter(const QStringList& pattern = {}); + virtual ~ExactListFilter() = default; + bool accepts(const QString& value) override; + + private: + QStringList m_pattern; +}; diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 201dcd572..1c2539e08 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -36,8 +36,6 @@ #include "GZip.h" #include #include -#include -#include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { @@ -138,82 +136,3 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) } return true; } - -int inf(QFile* source, std::function handleBlock) -{ - constexpr auto CHUNK = 16384; - int ret; - unsigned have; - z_stream strm; - memset(&strm, 0, sizeof(strm)); - char in[CHUNK]; - unsigned char out[CHUNK]; - - ret = inflateInit2(&strm, (16 + MAX_WBITS)); - if (ret != Z_OK) - return ret; - - /* decompress until deflate stream ends or end of file */ - do { - strm.avail_in = source->read(in, CHUNK); - if (source->error()) { - (void)inflateEnd(&strm); - return Z_ERRNO; - } - if (strm.avail_in == 0) - break; - strm.next_in = reinterpret_cast(in); - - /* run inflate() on input until output buffer not full */ - do { - strm.avail_out = CHUNK; - strm.next_out = out; - ret = inflate(&strm, Z_NO_FLUSH); - assert(ret != Z_STREAM_ERROR); /* state not clobbered */ - switch (ret) { - case Z_NEED_DICT: - ret = Z_DATA_ERROR; - [[fallthrough]]; - case Z_DATA_ERROR: - case Z_MEM_ERROR: - (void)inflateEnd(&strm); - return ret; - } - have = CHUNK - strm.avail_out; - if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { - (void)inflateEnd(&strm); - return Z_OK; - } - - } while (strm.avail_out == 0); - - /* done when inflate() says it's done */ - } while (ret != Z_STREAM_END); - - /* clean up and return */ - (void)inflateEnd(&strm); - return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; -} - -QString zerr(int ret) -{ - switch (ret) { - case Z_ERRNO: - return QObject::tr("error handling file"); - case Z_STREAM_ERROR: - return QObject::tr("invalid compression level"); - case Z_DATA_ERROR: - return QObject::tr("invalid or incomplete deflate data"); - case Z_MEM_ERROR: - return QObject::tr("out of memory"); - case Z_VERSION_ERROR: - return QObject::tr("zlib version mismatch!"); - } - return {}; -} - -QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) -{ - auto ret = inf(source, handleBlock); - return zerr(ret); -} diff --git a/launcher/GZip.h b/launcher/GZip.h index b736ca93f..0bdb70407 100644 --- a/launcher/GZip.h +++ b/launcher/GZip.h @@ -1,11 +1,8 @@ #pragma once #include -#include -namespace GZip { - -bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); -bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); -QString readGzFileByBlocks(QFile* source, std::function handleBlock); - -} // namespace GZip +class GZip { + public: + static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); + static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); +}; diff --git a/launcher/HardwareInfo.cpp b/launcher/HardwareInfo.cpp deleted file mode 100644 index 36b6f7783..000000000 --- a/launcher/HardwareInfo.cpp +++ /dev/null @@ -1,355 +0,0 @@ -// 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 . - */ - -#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 deleted file mode 100644 index 4efd339b6..000000000 --- a/launcher/HardwareInfo.h +++ /dev/null @@ -1,42 +0,0 @@ -// 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 087b37340..63c200cc4 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/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 1c3c0c984..61c51b3b7 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -8,23 +8,23 @@ struct InstanceCopyPrefs { public: - bool allTrue() const; - QString getSelectedFiltersAsRegex() const; - QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; + [[nodiscard]] bool allTrue() const; + [[nodiscard]] QString getSelectedFiltersAsRegex() const; + [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters - bool isCopySavesEnabled() const; - bool isKeepPlaytimeEnabled() const; - bool isCopyGameOptionsEnabled() const; - bool isCopyResourcePacksEnabled() const; - bool isCopyShaderPacksEnabled() const; - bool isCopyServersEnabled() const; - bool isCopyModsEnabled() const; - bool isCopyScreenshotsEnabled() const; - bool isUseSymLinksEnabled() const; - bool isLinkRecursivelyEnabled() const; - bool isUseHardLinksEnabled() const; - bool isDontLinkSavesEnabled() const; - bool isUseCloneEnabled() const; + [[nodiscard]] bool isCopySavesEnabled() const; + [[nodiscard]] bool isKeepPlaytimeEnabled() const; + [[nodiscard]] bool isCopyGameOptionsEnabled() const; + [[nodiscard]] bool isCopyResourcePacksEnabled() const; + [[nodiscard]] bool isCopyShaderPacksEnabled() const; + [[nodiscard]] bool isCopyServersEnabled() const; + [[nodiscard]] bool isCopyModsEnabled() const; + [[nodiscard]] bool isCopyScreenshotsEnabled() const; + [[nodiscard]] bool isUseSymLinksEnabled() const; + [[nodiscard]] bool isLinkRecursivelyEnabled() const; + [[nodiscard]] bool isUseHardLinksEnabled() const; + [[nodiscard]] bool isDontLinkSavesEnabled() const; + [[nodiscard]] bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index e32cdf095..0220a4144 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -3,12 +3,12 @@ #include #include #include "FileSystem.h" -#include "Filter.h" #include "NullInstance.h" +#include "pathmatcher/RegexpMatcher.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" -InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs) +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); @@ -30,8 +30,9 @@ InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCop if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... - QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); - m_matcher = Filters::regexp(regexp); + auto matcherReal = new RegexpMatcher(filters); + matcherReal->caseSensitive(false); + m_matcher.reset(matcherReal); } } @@ -42,7 +43,7 @@ void InstanceCopyTask::executeTask() m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); - folderClone.matcher(m_matcher); + folderClone.matcher(m_matcher.get()); folderClone(true); setProgress(0, folderClone.totalCloned()); @@ -64,13 +65,14 @@ 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); }); } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder - folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); + folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); folderLink(true); setProgress(0, m_progressTotal + folderLink.totalToLink()); @@ -89,7 +91,7 @@ void InstanceCopyTask::executeTask() QEventLoop loop; bool got_priv_results = false; - connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) { + connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { if (!gotResults) { qDebug() << "Privileged run exited without results!"; } @@ -125,11 +127,11 @@ void InstanceCopyTask::executeTask() return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.matcher(m_matcher); + folderCopy.followSymlinks(false).matcher(m_matcher.get()); folderCopy(true); setProgress(0, folderCopy.totalCopied()); - connect(&folderCopy, &FS::copy::fileCopied, [this]() { setProgress(m_progress + 1, m_progressTotal); }); + connect(&folderCopy, &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); @@ -146,9 +148,9 @@ void InstanceCopyTask::copyFinished() } // FIXME: shouldn't this be able to report errors? - auto instanceSettings = std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")); + auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); - BaseInstance* inst(new NullInstance(m_globalSettings, std::move(instanceSettings), m_stagingPath)); + InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); if (!m_keepPlaytime) { @@ -196,4 +198,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 a926af8a7..0f7f1020d 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -5,7 +5,6 @@ #include #include "BaseInstance.h" #include "BaseVersion.h" -#include "Filter.h" #include "InstanceCopyPrefs.h" #include "InstanceTask.h" #include "net/NetJob.h" @@ -15,7 +14,7 @@ class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs); + explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. @@ -26,10 +25,10 @@ class InstanceCopyTask : public InstanceTask { private: /* data */ - BaseInstance* m_origInstance; + InstancePtr m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; - Filter m_matcher; + std::unique_ptr m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index e58926660..bd3514798 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,25 +2,7 @@ #include #include - -#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(); -} +#include "FileSystem.h" void InstanceCreationTask::executeTask() { @@ -37,15 +19,13 @@ void InstanceCreationTask::executeTask() return; } - m_instance = createInstance(); - if (!m_instance) { - if (m_abort) { + if (!createInstance()) { + 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.")); @@ -64,10 +44,9 @@ void InstanceCreationTask::executeTask() setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (const QString& path : m_filesToRemove) { - if (!QFile::exists(path)) { + for (const QString& path : m_files_to_remove) { + if (!QFile::exists(path)) continue; - } qDebug() << "Removing" << path; @@ -82,61 +61,6 @@ void InstanceCreationTask::executeTask() return; } } - - 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")); - } - } + if (!m_abort) + emitSucceeded(); } diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 39acaf8b2..84fb2a145 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -2,7 +2,6 @@ #include "BaseVersion.h" #include "InstanceTask.h" -#include "minecraft/MinecraftInstance.h" class InstanceCreationTask : public InstanceTask { Q_OBJECT @@ -10,8 +9,6 @@ class InstanceCreationTask : public InstanceTask { InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; - bool abort() override; - protected: void executeTask() final override; @@ -30,24 +27,20 @@ class InstanceCreationTask : public InstanceTask { /** * Creates a new instance. * - * Returns the instance if it was created or nullptr otherwise. + * Returns whether the instance creation was successful (true) or not (false). */ - virtual std::unique_ptr createInstance() { return nullptr; } + virtual bool createInstance() { return false; }; 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_filesToRemove; - ShouldDeleteSaves m_shouldDeleteSaves; + QStringList m_files_to_remove; private: QString m_error_message; - std::unique_ptr m_instance; - Task::Ptr m_gameFilesTask; }; diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp deleted file mode 100644 index 75fbdb6c6..000000000 --- a/launcher/InstanceDirUpdate.cpp +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "InstanceDirUpdate.h" - -#include - -#include "Application.h" -#include "FileSystem.h" - -#include "InstanceList.h" -#include "ui/dialogs/CustomMessageBox.h" - -QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent) -{ - if (oldName == newName) - return QString(); - - QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); - if (renamingMode == "MetadataOnly") - return QString(); - - auto oldRoot = instance->instanceRoot(); - auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); - auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); - if (oldRoot == newRoot) - return QString(); - if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) - return QString(); - - // Check for conflict - if (QDir(newRoot).exists()) { - QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), - QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot)); - return QString(); - } - - // Ask if we should rename - if (renamingMode == "AskEverytime") { - auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); - auto dialog = - CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), - QObject::tr("Would you also like to rename the instance folder?\n\n" - "Old name: %1\n" - "New name: %2") - .arg(oldName, newName), - QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); - - auto res = dialog->exec(); - if (checkBox->isChecked()) { - if (res == QMessageBox::Yes) - APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); - else - APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); - } - if (res == QMessageBox::No) - return QString(); - } - - // Check for linked instances - if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) - return QString(); - - // Now we can confirm that a renaming is happening - if (!instance->syncInstanceDirName(newRoot)) { - QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), - QObject::tr("An error occurred when performing the following renaming operation:
" - " - Old instance root: %1
" - " - New instance root: %2
" - "Only the metadata is renamed.") - .arg(oldRoot, newRoot)); - return QString(); - } - return newRoot; -} - -bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) -{ - auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); - if (!linkedInstances.empty()) { - auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), - QObject::tr("The following instance(s) might reference files in this instance:\n\n" - "%1\n\n" - "%2 it could break the other instance(s), \n\n" - "Do you wish to proceed?", - nullptr, linkedInstances.count()) - .arg(linkedInstances.join("\n")) - .arg(verb), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - if (response != QMessageBox::Yes) - return false; - } - return true; -} diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h deleted file mode 100644 index 9da49a9a6..000000000 --- a/launcher/InstanceDirUpdate.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "BaseInstance.h" - -/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened -QString askToUpdateInstanceDirName(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 9b04f99b6..71630656d 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -38,11 +38,10 @@ #include "Application.h" #include "FileSystem.h" +#include "MMCZip.h" #include "NullInstance.h" #include "QObjectPtr.h" -#include "archive/ArchiveReader.h" -#include "archive/ExtractZipTask.h" #include "icons/IconList.h" #include "icons/IconUtils.h" @@ -55,10 +54,12 @@ #include "net/ApiDownload.h" -#include #include +#include #include +#include + InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) {} @@ -71,6 +72,7 @@ bool InstanceImportTask::abort() bool wasAborted = false; if (m_task) wasAborted = m_task->abort(); + Task::abort(); return wasAborted; } @@ -108,14 +110,39 @@ void InstanceImportTask::downloadFromUrl() filesNetJob->start(); } -QString cleanPath(QString path) +QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) { - if (path == ".") - return QString(); - QString result = path; - if (result.startsWith("./")) - result = result.mid(2); - return result; + if (!isRunning()) { + return {}; + } + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) { + setDetails(fileName); + if (fileName == "instance.cfg") { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + return root; + } + if (fileName == "manifest.json") { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + return root; + } + + QCoreApplication::processEvents(); + } + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { + if ("overrides/" == fileName) + continue; + + QString result = getRootFromZip(zip, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; } void InstanceImportTask::processZipPack() @@ -125,47 +152,33 @@ void InstanceImportTask::processZipPack() qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - MMCZip::ArchiveReader packZip(m_archivePath); + auto packZip = std::make_shared(m_archivePath); + if (!packZip->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + + QuaZipDir packZipDir(packZip.get()); qDebug() << "Attempting to determine instance type"; QString root; + // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // https://docs.modrinth.com/docs/modpacks/format_definition/#storage - auto detectInstance = [this, &extractDir, &root](MMCZip::ArchiveReader::File* f, bool& stop) { - if (!isRunning()) { - stop = true; - return true; - } - auto fileName = f->filename(); - if (fileName == "modrinth.index.json") { - // process as Modrinth pack - qDebug() << "Modrinth:" << true; - m_modpackType = ModpackType::Modrinth; - stop = true; - } else if (fileName == "bin/modpack.jar" || fileName == "bin/version.json") { - // process as Technic pack - qDebug() << "Technic:" << true; - extractDir.mkpath("minecraft"); - extractDir.cd("minecraft"); - m_modpackType = ModpackType::Technic; - stop = 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; - }; - if (!packZip.parse(detectInstance)) { - emitFailed(tr("Unable to open supplied modpack zip file.")); - return; + if (packZipDir.exists("/modrinth.index.json")) { + // process as Modrinth pack + qDebug() << "Modrinth:" << true; + m_modpackType = ModpackType::Modrinth; + } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) { + // process as Technic pack + qDebug() << "Technic:" << true; + extractDir.mkpath("minecraft"); + extractDir.cd("minecraft"); + m_modpackType = ModpackType::Technic; + } else { + root = getRootFromZip(packZip.get()); + setDetails(""); } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -174,7 +187,7 @@ void InstanceImportTask::processZipPack() setStatus(tr("Extracting modpack")); // make sure we extract just the pack - auto zipTask = makeShared(m_archivePath, extractDir, root); + auto zipTask = makeShared(packZip, extractDir, root); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { @@ -199,7 +212,6 @@ void InstanceImportTask::processZipPack() progressStep->status = status; stepProgress(*progressStep); }); - connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(zipTask); zipTask->start(); } @@ -251,25 +263,6 @@ void InstanceImportTask::extractFinished() } } -bool installIcon(QString root, QString instIconKey) -{ - auto importIconPath = IconUtils::findBestIconIn(root, instIconKey); - if (importIconPath.isNull() || !QFile::exists(importIconPath)) - importIconPath = IconUtils::findBestIconIn(root, "icon.png"); - if (importIconPath.isNull() || !QFile::exists(importIconPath)) - importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); - if (!importIconPath.isNull() && QFile::exists(importIconPath)) { - // import icon - auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(instIconKey)) { - iconList->deleteIcon(instIconKey); - } - iconList->installIcon(importIconPath, instIconKey + "." + QFileInfo(importIconPath).suffix()); - return true; - } - return false; -} - void InstanceImportTask::processFlame() { shared_qobject_ptr inst_creation_task = nullptr; @@ -295,14 +288,6 @@ void InstanceImportTask::processFlame() } inst_creation_task->setName(*this); - // if the icon was specified by user, use that. otherwise pull icon from the pack - if (m_instIcon == "default") { - auto iconKey = QString("Flame_%1_Icon").arg(name()); - - if (installIcon(m_stagingPath, iconKey)) { - m_instIcon = iconKey; - } - } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); @@ -320,11 +305,8 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); 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); }); m_task.reset(inst_creation_task); setAbortable(true); @@ -342,9 +324,9 @@ void InstanceImportTask::processTechnic() void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_unique(configPath); + auto instanceSettings = std::make_shared(configPath); - NullInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); + NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); // reset time played on import... because packs. instance.resetTimePlayed(); @@ -358,7 +340,17 @@ void InstanceImportTask::processMultiMC() } else { m_instIcon = instance.iconKey(); - installIcon(instance.instanceRoot(), m_instIcon); + auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png"); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(m_instIcon)) { + iconList->deleteIcon(m_instIcon); + } + iconList->installIcon(importIconPath, m_instIcon); + } } emitSucceeded(); } @@ -386,8 +378,8 @@ void InstanceImportTask::processModrinth() } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { - static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); - pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); + QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); + pack_id = regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs @@ -395,14 +387,6 @@ void InstanceImportTask::processModrinth() } inst_creation_task->setName(*this); - // if the icon was specified by user, use that. otherwise pull icon from the pack - if (m_instIcon == "default") { - auto iconKey = QString("Modrinth_%1_Icon").arg(name()); - - if (installIcon(m_stagingPath, iconKey)) { - m_instIcon = iconKey; - } - } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); @@ -420,11 +404,8 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); + connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); 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); }); m_task.reset(inst_creation_task); setAbortable(true); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index c92e229a0..8884e0801 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -40,6 +40,8 @@ #include #include "InstanceTask.h" +class QuaZip; + class InstanceImportTask : public InstanceTask { Q_OBJECT public: @@ -56,6 +58,7 @@ class InstanceImportTask : public InstanceTask { void processTechnic(); void processFlame(); void processModrinth(); + QString getRootFromZip(QuaZip* zip, const QString& root = ""); private slots: void processZipPack(); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 1339499c7..e1fa755dd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -34,24 +34,28 @@ * 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" @@ -59,12 +63,12 @@ #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 -#include +#include #endif const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent) +InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); @@ -138,7 +142,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()); } @@ -148,15 +152,15 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return count(); + return m_instances.count(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); - if (row < 0 || row >= count()) + if (row < 0 || row >= m_instances.size()) return QModelIndex(); - return createIndex(row, column, m_instances.at(row).get()); + return createIndex(row, column, (void*)m_instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const @@ -261,7 +265,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) if (changed) { increaseGroupCount(name); - auto idx = getInstIndex(inst); + auto idx = getInstIndex(inst.get()); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } @@ -329,7 +333,7 @@ bool InstanceList::trashInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; return false; } @@ -344,43 +348,26 @@ bool InstanceList::trashInstance(const InstanceId& id) } if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { - qWarning() << "Trash of instance" << id << "has not been completely successful..."; + qDebug() << "Trash of instance" << id << "has not been completely successfully..."; return false; } qDebug() << "Instance" << id << "has been trashed by the launcher."; m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); - // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway - for (const auto& [name, filePath, target] : inst->shortcuts()) { - if (!FS::trash(filePath, &trashedLoc)) { - qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id - << "has not been successful, trying to delete it instead..."; - if (!FS::deletePath(filePath)) { - qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id - << "has not been successful, given up..."; - } else { - qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; - } - continue; - } - qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; - m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); - } - return true; } -bool InstanceList::trashedSomething() const +bool InstanceList::trashedSomething() { return !m_trashHistory.empty(); } -bool InstanceList::undoTrashInstance() +void InstanceList::undoTrashInstance() { if (m_trashHistory.empty()) { qWarning() << "Nothing to recover from trash."; - return true; + return; } auto top = m_trashHistory.pop(); @@ -390,41 +377,21 @@ bool InstanceList::undoTrashInstance() top.path += "1"; } - if (!QFile(top.trashPath).rename(top.path)) { - qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; - return false; - } qDebug() << "Moving" << top.trashPath << "back to" << top.path; - - bool ok = true; - for (const auto& [data, trashPath] : top.shortcuts) { - if (QDir(data.filePath).exists()) { - // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it - qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; - ok = false; - continue; - } - if (!QFile(trashPath).rename(data.filePath)) { - qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; - ok = false; - continue; - } - qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; - } + QFile(top.trashPath).rename(top.path); m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); - return ok; } void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; + qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } @@ -437,22 +404,14 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Will delete instance" << id; if (!FS::deletePath(inst->instanceRoot())) { - qWarning() << "Deletion of instance" << id << "has not been completely successful..."; + qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; return; } qDebug() << "Instance" << id << "has been deleted by the launcher."; - - for (const auto& [name, filePath, target] : inst->shortcuts()) { - if (!FS::deletePath(filePath)) { - qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; - continue; - } - qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; - } } -static QMap getIdMapping(const std::vector>& list) +static QMap getIdMapping(const QList& list) { QMap out; int i = 0; @@ -461,7 +420,7 @@ static QMap getIdMapping(const std::vector getIdMapping(const std::vector InstanceList::discoverInstances() { - qInfo() << "Discovering instances in" << m_instDir; + qDebug() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { @@ -488,9 +447,13 @@ QList InstanceList::discoverInstances() } auto id = dirInfo.fileName(); out.append(id); - qInfo() << "Found instance ID" << id; + qDebug() << "Found instance ID" << id; } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) instanceSet = QSet(out.begin(), out.end()); +#else + instanceSet = out.toSet(); +#endif m_instancesProbed = true; return out; } @@ -499,16 +462,17 @@ InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); - std::vector> newList; + QList newList; for (auto& id : discoverInstances()) { if (existingIds.contains(id)) { + auto instPair = existingIds[id]; existingIds.remove(id); - qInfo() << "Should keep and soft-reload" << id; + qDebug() << "Should keep and soft-reload" << id; } else { - std::unique_ptr instPtr = loadInstance(id); + InstancePtr instPtr = loadInstance(id); if (instPtr) { - newList.push_back(std::move(instPtr)); + newList.append(instPtr); } } } @@ -523,7 +487,7 @@ InstanceList::InstListError InstanceList::loadList() int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { + auto removeNow = [&]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); @@ -560,8 +524,8 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for (const auto& itr : m_instances) { - totalPlayTime += itr->totalTimePlayed(); + for (auto const& itr : m_instances) { + totalPlayTime += itr.get()->totalTimePlayed(); } } @@ -572,12 +536,12 @@ void InstanceList::saveNow() } } -void InstanceList::add(std::vector>& t) +void InstanceList::add(const QList& t) { - beginInsertRows(QModelIndex(), count(), static_cast(count() + t.size() - 1)); + beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); + m_instances.append(t); for (auto& ptr : t) { - m_instances.push_back(std::move(ptr)); - connect(m_instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } @@ -607,26 +571,26 @@ void InstanceList::providerUpdated() } } -BaseInstance* InstanceList::getInstanceById(QString instId) const +InstancePtr InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) - return nullptr; + return InstancePtr(); for (auto& inst : m_instances) { if (inst->id() == instId) { - return inst.get(); + return inst; } } - return nullptr; + return InstancePtr(); } -BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name) const +InstancePtr 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.get(); + return instance; } return {}; @@ -634,14 +598,14 @@ BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { - return index(getInstIndex(getInstanceById(id))); + return index(getInstIndex(getInstanceById(id).get())); } int InstanceList::getInstIndex(BaseInstance* inst) const { - int count = this->count(); + int count = m_instances.count(); for (int i = 0; i < count; i++) { - if (inst == m_instances.at(i).get()) { + if (inst == m_instances[i].get()) { return i; } } @@ -657,15 +621,15 @@ void InstanceList::propertiesChanged(BaseInstance* inst) } } -std::unique_ptr InstanceList::loadInstance(const InstanceId& id) +InstancePtr InstanceList::loadInstance(const InstanceId& id) { if (!m_groupsLoaded) { loadGroupList(); } auto instanceRoot = FS::PathCombine(m_instDir, id); - auto instanceSettings = std::make_unique(FS::PathCombine(instanceRoot, "instance.cfg")); - std::unique_ptr inst; + auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); + InstancePtr inst; instanceSettings->registerSetting("InstanceType", ""); @@ -674,16 +638,11 @@ std::unique_ptr 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, std::move(instanceSettings), instanceRoot)); + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); } else { - inst.reset(new NullInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); } - qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); - - auto shortcut = inst->shortcuts(); - if (!shortcut.isEmpty()) - qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); - + qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); return inst; } @@ -905,20 +864,20 @@ class InstanceStaging : public Task { const unsigned maxBackoff = 16; public: - InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObject* settings) : m_parent(parent), backoff(minBackoff, maxBackoff) + InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) + : m_parent(parent), backoff(minBackoff, maxBackoff) { m_stagingPath = parent->getStagedInstancePath(); m_child.reset(child); m_child->setStagingPath(m_stagingPath); - m_child->setParentSettings(settings); + m_child->setParentSettings(std::move(settings)); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); 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); @@ -926,21 +885,22 @@ class InstanceStaging : public Task { connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } - ~InstanceStaging() override = default; + virtual ~InstanceStaging() {} // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if (!canAbort()) { + if (!canAbort()) return false; - } - return m_child->abort(); + m_child->abort(); + + return Task::abort(); } bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: - void executeTask() override + virtual void executeTask() override { if (m_stagingPath.isNull()) { emitFailed(tr("Could not create staging folder")); @@ -955,14 +915,12 @@ class InstanceStaging : public Task { void childSucceeded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) { - m_backoffTimer.stop(); + if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { 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; } @@ -971,14 +929,12 @@ 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(); } @@ -992,7 +948,7 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - std::unique_ptr m_child; + unique_qobject_ptr m_child; QTimer m_backoffTimer; }; @@ -1025,14 +981,15 @@ QString InstanceList::getStagedInstancePath() } bool InstanceList::commitStagedInstance(const QString& path, - const InstanceName& instanceName, + InstanceName const& instanceName, QString groupName, - const InstanceTask& commiting) + InstanceTask const& 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 f0a92d273..c85fe55c7 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -50,30 +50,24 @@ 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 }; enum class GroupsState { NotLoaded, Steady, Dirty }; -struct TrashShortcutItem { - ShortcutData data; - QString trashPath; -}; - struct TrashHistoryItem { QString id; QString path; QString trashPath; QString groupName; - QList shortcuts; }; class InstanceList : public QAbstractListModel { Q_OBJECT public: - explicit InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent = 0); + explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: @@ -96,17 +90,17 @@ class InstanceList : public QAbstractListModel { */ enum InstListError { NoError = 0, UnknownError }; - BaseInstance* at(int i) const { return m_instances.at(i).get(); } + InstancePtr at(int i) const { return m_instances.at(i); } - int count() const { return static_cast(m_instances.size()); } + int count() const { return m_instances.count(); } InstListError loadList(); void saveNow(); /* O(n) */ - BaseInstance* getInstanceById(QString id) const; + InstancePtr getInstanceById(QString id) const; /* O(n) */ - BaseInstance* getInstanceByManagedName(const QString& managed_name) const; + InstancePtr getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString& id) const; QStringList getGroups(); bool isGroupCollapsed(const QString& groupName); @@ -117,8 +111,8 @@ class InstanceList : public QAbstractListModel { void deleteGroup(const GroupId& name); void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); - bool trashedSomething() const; - bool undoTrashInstance(); + bool trashedSomething(); + void undoTrashInstance(); void deleteInstance(const InstanceId& id); // Wrap an instance creation task in some more task machinery and make it ready to be used @@ -179,11 +173,11 @@ class InstanceList : public QAbstractListModel { void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); - void add(std::vector>& list); + void add(const QList& list); void loadGroupList(); void saveGroupList(); QList discoverInstances(); - std::unique_ptr loadInstance(const InstanceId& id); + InstancePtr loadInstance(const InstanceId& id); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); @@ -192,11 +186,11 @@ class InstanceList : public QAbstractListModel { int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; - std::vector> m_instances; + QList m_instances; // id -> refs QMap m_groupNameCache; - SettingsObject* m_globalSettings; + SettingsObjectPtr 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 134fb8f24..174041f89 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" @@ -21,36 +20,39 @@ class InstancePageProvider : protected QObject, public BasePageProvider { Q_OBJECT public: - explicit InstancePageProvider(BaseInstance* parent) { inst = parent; } + explicit InstancePageProvider(InstancePtr parent) { inst = parent; } virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; values.append(new LogPage(inst)); - MinecraftInstance* onesix = dynamic_cast(inst); - values.append(new VersionPage(onesix)); - values.append(ManagedPackPage::createPage(onesix)); - auto modsPage = new ModFolderPage(onesix, onesix->loaderModList()); + 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()); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); values.append(modsPage); - 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 CoreModFolderPage(onesix.get(), onesix->coreModList())); + values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); + values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); + values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); + values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); + values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); + // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); - values.append(new InstanceSettingsPage(onesix)); - values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); + values.append(new InstanceSettingsPage(onesix.get())); + auto logMatcher = inst->getLogFileMatcher(); + if (logMatcher) { + values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); + } return values; } virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: - BaseInstance* inst; + InstancePtr inst; }; diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 01998a7aa..be10bbe07 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,5 +1,4 @@ #include "InstanceTask.h" -#include #include "Application.h" #include "settings/SettingsObject.h" @@ -83,13 +82,3 @@ 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 125930a27..7c02160a7 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -8,18 +8,16 @@ 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: InstanceName() = default; InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} - QString modifiedName() const; - QString originalName() const; - QString name() const; - QString version() const; + [[nodiscard]] QString modifiedName() const; + [[nodiscard]] QString originalName() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString version() const; void setName(QString name) { m_modified_name = name; } void setName(InstanceName& other); @@ -37,7 +35,7 @@ class InstanceTask : public Task, public InstanceName { InstanceTask(); ~InstanceTask() override = default; - void setParentSettings(SettingsObject* settings) { m_globalSettings = settings; } + void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; } void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } @@ -46,12 +44,12 @@ class InstanceTask : public Task, public InstanceName { void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } - bool shouldConfirmUpdate() const { return m_confirm_update; } + [[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; } void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } bool shouldOverride() const { return m_override_existing; } - QString originalInstanceID() const { return m_original_instance_id; }; + [[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; }; protected: void setOverride(bool override, QString instance_id_to_override = {}) @@ -62,7 +60,7 @@ class InstanceTask : public Task, public InstanceName { } protected: /* data */ - SettingsObject* m_globalSettings; + SettingsObjectPtr m_globalSettings; QString m_instIcon; QString m_instGroup; QString m_stagingPath; diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 7bb674dde..188edb943 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -41,9 +41,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { - static const QRegularExpression s_memRegex("-Xm[sx]"); - static const QRegularExpression s_versionRegex("-version:.*"); - if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " @@ -54,7 +52,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return false; } // block lunacy with passing required version to the JVM - if (jvmargs.contains(s_versionRegex)) { + if (jvmargs.contains(QRegularExpression("-version:.*"))) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" @@ -95,7 +93,7 @@ void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& re { QString text; text += QObject::tr( - "The specified Java binary didn't work.
You should press 'Detect', " + "The specified Java binary didn't work.
You should use the auto-detect feature, " "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index 0e4aa2b0a..a21b5a494 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -24,7 +24,7 @@ class TestCheck : public QObject { TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} - virtual ~TestCheck() = default; + virtual ~TestCheck() {}; void run(); diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 2d3372e2e..f397f89c5 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -101,21 +101,6 @@ QJsonArray requireArray(const QJsonDocument& doc, const QString& what) return doc.array(); } -QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error, QString* garbage) -{ - auto doc = QJsonDocument::fromJson(json, error); - if (error->error == QJsonParseError::GarbageAtEnd) { - qsizetype offset = error->offset; - QByteArray validJson = json.left(offset); - doc = QJsonDocument::fromJson(validJson, error); - - if (garbage) - *garbage = json.right(json.size() - offset); - } - - return doc; -} - void writeString(QJsonObject& to, const QString& key, const QString& value) { if (!value.isEmpty()) { @@ -168,7 +153,7 @@ QJsonValue toJson(const QVariant& variant) template <> QByteArray requireIsType(const QJsonValue& value, const QString& what) { - const QString string = value.toString(what); + const QString string = ensureIsType(value, what); // ensure that the string can be safely cast to Latin1 if (string != QString::fromLatin1(string.toLatin1())) { throw JsonException(what + " is not encodable as Latin1"); @@ -236,7 +221,7 @@ QDateTime requireIsType(const QJsonValue& value, const QString& what) template <> QUrl requireIsType(const QJsonValue& value, const QString& what) { - const QString string = value.toString(what); + const QString string = ensureIsType(value, what); if (string.isEmpty()) { return QUrl(); } @@ -294,48 +279,4 @@ QJsonValue requireIsType(const QJsonValue& value, const QString& wha return value; } -QStringList toStringList(const QString& jsonString) -{ - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError || !doc.isArray()) - return {}; - try { - return requireIsArrayOf(doc); - } catch (Json::JsonException&) { - return {}; - } -} - -QString fromStringList(const QStringList& list) -{ - QJsonArray array; - for (const QString& str : list) { - array.append(str); - } - - QJsonDocument doc(toJsonArray(list)); - return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); -} - -QVariantMap toMap(const QString& jsonString) -{ - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError || !doc.isObject()) - return {}; - - QJsonObject obj = doc.object(); - return obj.toVariantMap(); -} - -QString fromMap(const QVariantMap& map) -{ - QJsonObject obj = QJsonObject::fromVariantMap(map); - QJsonDocument doc(obj); - return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); -} - } // namespace Json diff --git a/launcher/Json.h b/launcher/Json.h index 7a50af167..28891f398 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -99,7 +99,7 @@ template QJsonArray toJsonArray(const QList& container) { QJsonArray array; - for (const T& item : container) { + for (const T item : container) { array.append(toJson(item)); } return array; @@ -107,9 +107,6 @@ QJsonArray toJsonArray(const QList& container) ////////////////// READING //////////////////// -// Attempt to parse JSON up until garbage is encountered -QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error = nullptr, QString* garbage = nullptr); - /// @throw JsonException template T requireIsType(const QJsonValue& value, const QString& what = "Value"); @@ -156,6 +153,18 @@ QUrl requireIsType(const QJsonValue& value, const QString& what); // the following functions are higher level functions, that make use of the above functions for // type conversion +template +T ensureIsType(const QJsonValue& value, const T default_ = T(), const QString& what = "Value") +{ + if (value.isUndefined() || value.isNull()) { + return default_; + } + try { + return requireIsType(value, what); + } catch (const JsonException&) { + return default_; + } +} /// @throw JsonException template @@ -169,31 +178,68 @@ T requireIsType(const QJsonObject& parent, const QString& key, const QString& wh } template -QList requireIsArrayOf(const QJsonDocument& doc) +T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = T(), const QString& what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) { + return default_; + } + return ensureIsType(parent.value(key), default_, localWhat); +} + +template +QVector requireIsArrayOf(const QJsonDocument& doc) { const QJsonArray array = requireArray(doc); - QList out; + QVector out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } return out; } +template +QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") +{ + const QJsonArray array = ensureIsType(value, QJsonArray(), what); + QVector out; + for (const QJsonValue val : array) { + out.append(requireIsType(val, what)); + } + return out; +} + +template +QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value") +{ + if (value.isUndefined()) { + return default_; + } + return ensureIsArrayOf(value, what); +} + /// @throw JsonException template -QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") +QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { throw JsonException(localWhat + "s parent does not contain " + localWhat); } + return ensureIsArrayOf(parent.value(key), localWhat); +} - const QJsonArray array = parent[key].toArray(); - QList out; - for (const QJsonValue val : array) { - out.append(requireIsType(val, "Document")); +template +QVector ensureIsArrayOf(const QJsonObject& parent, + const QString& key, + const QVector& default_ = QVector(), + const QString& what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) { + return default_; } - return out; + return ensureIsArrayOf(parent.value(key), default_, localWhat); } // this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers @@ -202,9 +248,18 @@ QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const Q { \ return requireIsType(value, what); \ } \ + inline TYPE ensure##NAME(const QJsonValue& value, const TYPE default_ = TYPE(), const QString& what = "Value") \ + { \ + return ensureIsType(value, default_, what); \ + } \ inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \ { \ return requireIsType(parent, key, what); \ + } \ + inline TYPE ensure##NAME(const QJsonObject& parent, const QString& key, const TYPE default_ = TYPE(), \ + const QString& what = "__placeholder") \ + { \ + return ensureIsType(parent, key, default_, what); \ } JSON_HELPERFUNCTIONS(Array, QJsonArray) @@ -223,12 +278,5 @@ JSON_HELPERFUNCTIONS(Variant, QVariant) #undef JSON_HELPERFUNCTIONS -// helper functions for settings -QStringList toStringList(const QString& jsonString); -QString fromStringList(const QStringList& list); - -QVariantMap toMap(const QString& jsonString); -QString fromMap(const QVariantMap& map); - } // namespace Json using JSONValidationError = Json::JsonException; diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 2b882338c..aaf84eac4 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -40,26 +40,28 @@ #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" #include "launch/steps/TextPrint.h" #include "tasks/Task.h" -#include "ui/dialogs/ChooseOfflineNameDialog.h" -LaunchController::LaunchController() = default; +LaunchController::LaunchController() : Task() {} void LaunchController::executeTask() { @@ -82,17 +84,9 @@ void LaunchController::decideAccount() return; } - // 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()) { + // Find an account to use. + auto accounts = APPLICATION->accounts(); + if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " @@ -110,7 +104,16 @@ void LaunchController::decideAccount() } } - if (!m_accountToUse && 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 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 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); @@ -127,150 +130,38 @@ void LaunchController::decideAccount() } } -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 +bool LaunchController::askPlayDemo() { QMessageBox box(m_parentWidget); box.setWindowTitle(tr("Play 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.setText( + tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " + "the demo?")); box.setIcon(QMessageBox::Warning); - const auto* demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); - auto* cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + 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(const QString& playerName, bool* ok) +QString LaunchController::askOfflineName(QString playerName, bool demo, 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; - } - - const QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; - - ChooseOfflineNameDialog dialog(message, m_parentWidget); - dialog.setWindowTitle(title); - dialog.setUsername(usedname); - if (dialog.exec() != QDialog::Accepted) { + QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok); + if (!ok) return {}; - } - - usedname = dialog.getUsername(); - APPLICATION->settings()->set("LastOfflinePlayerName", usedname); - - if (ok != nullptr) { - *ok = true; + if (name.length()) { + usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); } return usedname; } @@ -279,96 +170,154 @@ void LaunchController::login() { decideAccount(); - 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()) { + if (!m_accountToUse) { + // if no account is selected, ask about demo + if (!m_demo) { + m_demo = askPlayDemo(); + } + if (m_demo) { + // we ask the user for a player name bool ok = false; - auto name = askOfflineName("Player", &ok); + auto name = askOfflineName("Player", m_demo, ok); if (ok) { m_session = std::make_shared(); - m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString(QUuid::Id128)); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]"))); launchInstance(); return; } } - - emitFailed(tr("No account selected for launch")); + // if no account is selected, we bail + emitFailed(tr("No account selected for launch.")); return; } - m_session = std::make_shared(); - m_session->launchMode = m_actualLaunchMode; - m_accountToUse->fillSession(m_session); + // we loop until the user succeeds in logging in or gives up + bool tryagain = true; + unsigned int tries = 0; - 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) { + if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || + m_accountToUse->shouldRefresh()) { + // Force account refresh on the account used to launch the instance updating the AccountState + // only on first try and if it is not meant to be offline + auto accounts = APPLICATION->accounts(); + accounts->requestRefresh(m_accountToUse->internalId()); + } + while (tryagain) { + if (tries > 0 && tries % 3 == 0) { + auto result = + 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) { emitAborted(); return; } } + tries++; + m_session = std::make_shared(); + m_session->wants_online = m_online; + m_session->demo = m_demo; + m_accountToUse->fillSession(m_session); - 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(); + // Launch immediately in true offline mode + if (m_accountToUse->accountType() == AccountType::Offline) { + launchInstance(); + return; + } + + switch (m_accountToUse->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; + auto name = askOfflineName(m_session->player_name, m_session->demo, ok); + if (!ok) { + tryagain = false; + break; + } + m_session->MakeOffline(name); + // offline flavored game from here :3 + } + if (m_accountToUse->ownsMinecraft()) { + if (!m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) { + tryagain = true; + continue; + } else { + emitFailed(tr("Received undetermined session status during login.")); + return; + } + } + // we own Minecraft, there is a profile, it's all ready to go! + launchInstance(); return; + } else { + // play demo ? + if (!m_session->demo) { + m_session->demo = askPlayDemo(); + } + if (m_session->demo) { // play demo here + launchInstance(); + } else { + emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + } } + return; } - m_session->MakeOffline(name); - } - } - - launchInstance(); -} - -bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason) -{ - auto button = QMessageBox::warning( - 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(); - 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) { - accounts->setDefaultAccount(newAccount); + case AccountState::Errored: + // This means some sort of soft error that we can fix with a refresh ... so let's refresh. + case AccountState::Unchecked: { + m_accountToUse->refresh(); + } + /* fallthrough */ + case AccountState::Working: { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + if (m_online) { + progDialog.setSkipButton(true, tr("Play Offline")); } - - if (m_accountToUse == account) { - m_accountToUse = nullptr; - decideAccount(); - } - return true; + auto task = m_accountToUse->currentTask(); + progDialog.execWithTask(task.get()); + continue; + } + case AccountState::Expired: { + auto errorString = tr("The account has expired and needs to be logged into manually again."); + QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok); + emitFailed(errorString); + return; + } + case AccountState::Disabled: { + auto errorString = tr("The launcher's client identification has changed. Please remove this account and add it again."); + QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok); + emitFailed(errorString); + return; + } + case AccountState::Gone: { + auto errorString = + tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account " + "you migrated this one to."); + QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok); + emitFailed(errorString); + return; } } } - - return false; + emitFailed(tr("Failed to launch.")); } void LaunchController::launchInstance() { - Q_ASSERT(m_instance != nullptr); - Q_ASSERT(m_session.get() != nullptr); + Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); + Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); if (!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); @@ -382,36 +331,37 @@ void LaunchController::launchInstance() return; } - const auto* console = qobject_cast(m_parentWidget); - const auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + auto console = qobject_cast(m_parentWidget); + auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); if (!console && showConsole) { APPLICATION->showInstanceWindow(m_instance); } - 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); + 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); // Prepend Online and Auth Status QString online_mode; - if (m_actualLaunchMode == LaunchMode::Normal) { + if (m_session->wants_online) { online_mode = "online"; // Prepend Server Status - const QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; + QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - m_launcher->prependStep(makeShared(m_launcher, servers)); + m_launcher->prependStep(makeShared(m_launcher.get(), servers)); } else { - online_mode = m_actualLaunchMode == LaunchMode::Demo ? "demo" : "offline"; + online_mode = m_demo ? "demo" : "offline"; } - m_launcher->prependStep(makeShared(m_launcher, "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); + m_launcher->prependStep( + makeShared(m_launcher.get(), "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, versionString + "\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); } m_launcher->start(); } @@ -468,10 +418,10 @@ void LaunchController::onFailed(QString reason) if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { APPLICATION->showInstanceWindow(m_instance, "console"); } - emitFailed(std::move(reason)); + emitFailed(reason); } -void LaunchController::onProgressRequested(Task* task) const +void LaunchController::onProgressRequested(Task* task) { ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index bc0d14e0f..af52ad450 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -36,29 +36,27 @@ #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(); - ~LaunchController() override = default; + virtual ~LaunchController() = default; - void setInstance(BaseInstance* instance) { m_instance = instance; } + void setInstance(InstancePtr instance) { m_instance = instance; } - BaseInstance* instance() const { return m_instance; } + InstancePtr instance() { return m_instance; } - void setLaunchMode(const LaunchMode mode) { m_wantedLaunchMode = mode; } + void setOnline(bool online) { m_online = online; } - void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } + void setDemo(bool demo) { m_demo = demo; } void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } @@ -68,7 +66,7 @@ class LaunchController : public Task { void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } - QString id() const { return m_instance->id(); } + QString id() { return m_instance->id(); } bool abort() override; @@ -76,28 +74,25 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); - LaunchDecision decideLaunchMode(); - bool askPlayDemo() const; - QString askOfflineName(const QString& playerName, bool* ok = nullptr); - bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason); + bool askPlayDemo(); + QString askOfflineName(QString playerName, bool demo, bool& ok); private slots: void readyForLaunch(); void onSucceeded(); void onFailed(QString reason); - void onProgressRequested(Task* task) const; + void onProgressRequested(Task* task); private: - LaunchMode m_wantedLaunchMode = LaunchMode::Normal; - LaunchMode m_actualLaunchMode = LaunchMode::Normal; BaseProfilerFactory* m_profiler = nullptr; - QString m_offlineName; - BaseInstance* m_instance = nullptr; + bool m_online = true; + bool m_demo = false; + InstancePtr m_instance; QWidget* m_parentWidget = nullptr; InstanceWindow* m_console = nullptr; MinecraftAccountPtr m_accountToUse = nullptr; - AuthSessionPtr m_session = nullptr; - LaunchTask* m_launcher = nullptr; - MinecraftTarget::Ptr m_targetToJoin = nullptr; + AuthSessionPtr m_session; + shared_qobject_ptr m_launcher; + MinecraftTarget::Ptr m_targetToJoin; }; diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 28ba32bf8..706d7022b 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -15,28 +15,92 @@ 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 +# Set up env. +# Pass our custom variables separately so that the launcher can remove them for child processes +export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@" +export LAUNCHER_LD_PRELOAD="" +export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" +export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts" -# 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 +export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" +export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD" +export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH" +export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH" + +# Detect missing dependencies... +DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'` +if [ "x$DEPS_LIST" = "x" ]; then + # We have all our dependencies. Run the launcher. + echo "No missing dependencies found." + + # Just to be sure... + chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" + + ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") + + if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") + fi + + ARGS+=("$@") + + # Run the launcher + exec -a "${ARGS[@]}" + + # Run the launcher in valgrind + # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + + # Run the launcher with callgrind, delay instrumentation + # valgrind --log-file="valgrind.log" --tool=callgrind --instr-atstart=no "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + # use callgrind_control -i on/off to profile actions + + # Exit with launcher's exit code. + # exit $? +else + # apt + if which apt-file &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" + # pacman + elif which pkgfile &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" + # dnf + elif which dnf &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do dnf whatprovides -q $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | grep -v 'Repo' | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo dnf install $COMMAND_LIBS" + # yum + elif which yum &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo yum install $COMMAND_LIBS" + # zypper + elif which zypper &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo zypper install $COMMAND_LIBS" + # emerge + elif which pfl &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo emerge $COMMAND_LIBS" + fi + + MESSAGE="Error: The launcher is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." + MESSAGE="$MESSAGE\n\nHint (please apply common sense): $INSTALL_CMD\n" + + printerror "$MESSAGE" + exit 1 fi - -# Just to be sure... -chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" - -ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") - -if [ -f portable.txt ]; then - ARGS+=("-d" "${LAUNCHER_DIR}") -fi - -ARGS+=("$@") - -# Run the launcher -exec -a "${ARGS[@]}" diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index c5ef47d7b..35ce4e0e5 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -36,16 +36,16 @@ #include "LoggedProcess.h" #include -#include +#include #include "MessageLevel.h" -LoggedProcess::LoggedProcess(const QStringConverter::Encoding output_codec, QObject* parent) +LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); - connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); + connect(this, QOverload::of(&QProcess::finished), this, &LoggedProcess::on_exit); connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } @@ -57,9 +57,9 @@ LoggedProcess::~LoggedProcess() } } -QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder) +QStringList LoggedProcess::reprocess(const QByteArray& data, QTextDecoder& decoder) { - QString str = decoder(data); + auto str = decoder.toUnicode(data); if (!m_leftover_line.isEmpty()) { str.prepend(m_leftover_line); @@ -114,7 +114,7 @@ void LoggedProcess::on_error(QProcess::ProcessError error) { switch (error) { case QProcess::FailedToStart: { - emit log({ tr("The process failed to start: %1").arg(errorString()) }, MessageLevel::Fatal); + emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); changeState(LoggedProcess::FailedToStart); break; } diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index ce35b27e8..75ba15dfd 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -36,7 +36,7 @@ #pragma once #include -#include +#include #include "MessageLevel.h" /* @@ -49,7 +49,7 @@ class LoggedProcess : public QProcess { enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: - explicit LoggedProcess(QStringConverter::Encoding outputEncoding = QStringConverter::System, QObject* parent = nullptr); + explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0); virtual ~LoggedProcess(); State state() const; @@ -58,7 +58,7 @@ class LoggedProcess : public QProcess { void setDetachable(bool detachable); signals: - void log(QStringList lines, MessageLevel level); + void log(QStringList lines, MessageLevel::Enum level); void stateChanged(LoggedProcess::State state); public slots: @@ -77,11 +77,11 @@ class LoggedProcess : public QProcess { private: void changeState(LoggedProcess::State state); - QStringList reprocess(const QByteArray& data, QStringDecoder& decoder); + QStringList reprocess(const QByteArray& data, QTextDecoder& decoder); private: - QStringDecoder m_err_decoder; - QStringDecoder m_out_decoder; + QTextDecoder m_err_decoder; + QTextDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 252e6ac57..1765fd844 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -16,6 +16,7 @@ */ #include +#include #include #include @@ -98,4 +99,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 64be89643..b38aca17a 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -35,46 +35,66 @@ */ #include "MMCZip.h" -#include +#include +#include +#include #include "FileSystem.h" -#include "archive/ArchiveReader.h" -#include "archive/ArchiveWriter.h" #include #include #include #include -#include + +#if defined(LAUNCHER_APPLICATION) +#include +#endif namespace MMCZip { // ours -using FilterFunction = std::function; -#if defined(LAUNCHER_APPLICATION) -bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr) +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter) { - ArchiveReader r(from.absoluteFilePath()); - return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { - auto filename = f->filename(); + QuaZip modZip(from.filePath()); + modZip.open(QuaZip::mdUnzip); + + QuaZipFile fileInsideMod(&modZip); + QuaZipFile zipOutFile(into); + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) { + QString filename = modZip.getCurrentFileName(); if (filter && !filter(filename)) { - qDebug() << "Skipping file" << filename << "from" << from.fileName() << "- filtered"; - f->skip(); - return true; + qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; + continue; } if (contained.contains(filename)) { - qDebug() << "Skipping already contained file" << filename << "from" << from.fileName(); - f->skip(); - return true; + qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); + continue; } contained.insert(filename); - if (!into.addFile(f)) { - qCritical() << "Failed to copy data of" << filename << "into the jar"; + + if (!fileInsideMod.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open " << filename << " from " << from.fileName(); return false; } - return true; - }); + + QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) { + qCritical() << "Failed to open " << filename << " in the jar"; + fileInsideMod.close(); + return false; + } + if (!JlCompress::copyData(fileInsideMod, zipOutFile)) { + zipOutFile.close(); + fileInsideMod.close(); + qCritical() << "Failed to copy data of " << filename << " into the jar"; + return false; + } + zipOutFile.close(); + fileInsideMod.close(); + } + return true; } -bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) +bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks) { QDir directory(dir); if (!directory.exists()) @@ -83,18 +103,48 @@ bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto srcPath = e.absoluteFilePath(); - if (!zip.addFile(srcPath, filePath)) + if (followSymlinks) { + if (e.isSymLink()) { + srcPath = e.symLinkTarget(); + } else { + srcPath = e.canonicalFilePath(); + } + } + if (!JlCompress::compressFile(zip, srcPath, filePath)) return false; } return true; } +bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) +{ + QuaZip zip(fileCompressed); + zip.setUtf8Enabled(true); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if (!zip.open(QuaZip::mdCreate)) { + FS::deletePath(fileCompressed); + return false; + } + + auto result = compressDirFiles(&zip, dir, files, followSymlinks); + + zip.close(); + if (zip.getZipError() != 0) { + FS::deletePath(fileCompressed); + return false; + } + + return result; +} + +#if defined(LAUNCHER_APPLICATION) // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { - ArchiveWriter zipOut(targetJarPath); - if (!zipOut.open()) { + QuaZip zipOut(targetJarPath); + zipOut.setUtf8Enabled(true); + if (!zipOut.open(QuaZip::mdCreate)) { FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; @@ -111,7 +161,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListenabled()) continue; if (mod->type() == ResourceType::ZIPFILE) { - if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) { + if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -120,7 +170,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListtype() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); - if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) { + if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -143,13 +193,13 @@ 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(); @@ -159,7 +209,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target) +QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) +{ + QuaZipDir rootDir(zip, root); + for (auto&& fileName : rootDir.entryList(QDir::Files)) { + if (fileName == what) + return root; + + QCoreApplication::processEvents(); + } + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { + if (ignore_paths.contains(fileName)) + continue; + + QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName); + if (!result.isEmpty()) + return result; + } + + return {}; +} + +// ours +bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root) +{ + QuaZipDir rootDir(zip, root); + for (auto fileName : rootDir.entryList(QDir::Files)) { + if (fileName == what) { + result.append(root); + return true; + } + } + for (auto fileName : rootDir.entryList(QDir::Dirs)) { + findFilesInZip(zip, what, result, root + fileName); + } + return !result.isEmpty(); +} + +// ours +std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - if (!zip->collectFiles()) { + auto numEntries = zip->getEntriesCount(); + if (numEntries < 0) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; - } - if (zip->getFiles().isEmpty()) { + } else if (numEntries == 0) { qDebug() << "Extracting empty archives seems odd..."; return extracted; - } - - auto extPtr = ArchiveWriter::createDiskWriter(); - auto ext = extPtr.get(); - - if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { - QString file_name = f->filename(); - file_name = FS::RemoveInvalidPathChars(file_name); - if (!file_name.startsWith(subdir)) { - f->skip(); - return true; - } - - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); - auto original_name = relative_file_name; - - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); - - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - - relative_file_name = relative_file_name.split('/').last(); - } - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } - - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" - << target; - return false; - } - if (!f->writeFile(ext, target_file_path, target)) { - qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; - return false; - } - - extracted.append(target_file_path); - - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - return true; - })) { - qWarning() << "Failed to parse file" << zip->getZipName(); - FS::removeFiles(extracted); + } else if (!zip->goToFirstFile()) { + qWarning() << "Failed to seek to first file in zip"; return std::nullopt; } + do { + QString file_name = zip->getCurrentFileName(); + file_name = FS::RemoveInvalidPathChars(file_name); + if (!file_name.startsWith(subdir)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); + auto original_name = relative_file_name; + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" + << target; + return std::nullopt; + } + + if (!JlCompress::extractFile(zip, "", target_file_path)) { + qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; + JlCompress::removeFile(extracted); + return std::nullopt; + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } else if (fileInfo.isDir()) { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | + QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) { + qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } while (zip->goToNextFile()); + return extracted; } +// ours +bool extractRelFile(QuaZip* zip, const QString& file, const QString& target) +{ + return JlCompress::extractFile(zip, file, target); +} + // ours std::optional extractDir(QString fileCompressed, QString dir) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return QStringList(); + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + ; + return std::nullopt; } - ArchiveReader zip(fileCompressed); return extractSubDir(&zip, "", dir); } // ours std::optional extractDir(QString fileCompressed, QString subdir, QString dir) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return QStringList(); + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + ; + return std::nullopt; } - ArchiveReader zip(fileCompressed); return extractSubDir(&zip, subdir, dir); } // ours bool extractFile(QString fileCompressed, QString file, QString target) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return true; - } - ArchiveReader zip(fileCompressed); - auto f = zip.goToFile(file); - if (!f) { + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return true; + } + qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); return false; } - auto extPtr = ArchiveWriter::createDiskWriter(); - auto ext = extPtr.get(); - - return f->writeFile(ext, target); + return extractRelFile(&zip, file, target); } -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) @@ -319,9 +443,9 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { - if (excludeFilter && excludeFilter(e)) { - QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); - qDebug() << "Skipping file" << relativeFilePath; + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); + if (excludeFilter && excludeFilter(relativeFilePath)) { + qDebug() << "Skipping file " << relativeFilePath; continue; } @@ -329,4 +453,218 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q } return true; } + +#if defined(LAUNCHER_APPLICATION) +void ExportToZipTask::executeTask() +{ + setStatus("Adding files..."); + setProgress(0, m_files.length()); + m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); + connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); + m_build_zip_watcher.setFuture(m_build_zip_future); +} + +auto ExportToZipTask::exportZip() -> ZipResult +{ + if (!m_dir.exists()) { + return ZipResult(tr("Folder doesn't exist")); + } + if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) { + return ZipResult(tr("Could not create file")); + } + + for (auto fileName : m_extra_files.keys()) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + QuaZipFile indexFile(&m_output); + if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) { + return ZipResult(tr("Could not create:") + fileName); + } + indexFile.write(m_extra_files[fileName]); + } + + for (const QFileInfo& file : m_files) { + if (m_build_zip_future.isCanceled()) + return ZipResult(); + + auto absolute = file.absoluteFilePath(); + auto relative = m_dir.relativeFilePath(absolute); + setStatus("Compressing: " + relative); + setProgress(m_progress + 1, m_progressTotal); + if (m_follow_symlinks) { + if (file.isSymLink()) + absolute = file.symLinkTarget(); + else + absolute = file.canonicalFilePath(); + } + + if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) { + return ZipResult(tr("Could not read and compress %1").arg(relative)); + } + } + + m_output.close(); + if (m_output.getZipError() != 0) { + return ZipResult(tr("A zip error occurred")); + } + return ZipResult(); +} + +void ExportToZipTask::finish() +{ + if (m_build_zip_future.isCanceled()) { + FS::deletePath(m_output_path); + emitAborted(); + } else if (auto result = m_build_zip_future.result(); result.has_value()) { + FS::deletePath(m_output_path); + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExportToZipTask::abort() +{ + if (m_build_zip_future.isRunning()) { + m_build_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +void ExtractZipTask::executeTask() +{ + if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zip_watcher.setFuture(m_zip_future); +} + +auto ExtractZipTask::extractZip() -> ZipResult +{ + auto target = m_output_dir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; + auto numEntries = m_input->getEntriesCount(); + if (numEntries < 0) { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (numEntries == 0) { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + if (!m_input->goToFirstFile()) { + return ZipResult(tr("Failed to seek to first file in zip")); + } + + setStatus("Extracting files..."); + setProgress(0, numEntries); + do { + if (m_zip_future.isCanceled()) + return ZipResult(); + setProgress(m_progress + 1, m_progressTotal); + QString file_name = m_input->getCurrentFileName(); + if (!file_name.startsWith(m_subdirectory)) + continue; + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unpacking: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + } + + if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) { + JlCompress::removeFile(extracted); + return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + } + + extracted.append(target_file_path); + auto fileInfo = QFileInfo(target_file_path); + if (fileInfo.isFile()) { + auto permissions = fileInfo.permissions(); + auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | + QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; + auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + + auto newPermisions = (permissions & maxPermisions) | minPermisions; + if (newPermisions != permissions) { + if (!QFile::setPermissions(target_file_path, newPermisions)) { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } else if (fileInfo.isDir()) { + // Ensure the folder has the minimal required permissions + QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | + QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; + + QFile::Permissions currentPermissions = fileInfo.permissions(); + if ((currentPermissions & minimalPermissions) != minimalPermissions) { + if (!QFile::setPermissions(target_file_path, minimalPermissions)) { + logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); + } + } + } + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + } while (m_input->goToNextFile()); + + return ZipResult(); +} + +void ExtractZipTask::finish() +{ + if (m_zip_future.isCanceled()) { + emitAborted(); + } else if (auto result = m_zip_future.result(); result.has_value()) { + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExtractZipTask::abort() +{ + if (m_zip_future.isRunning()) { + m_zip_future.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +#endif } // namespace MMCZip diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 04fe90379..d81df9d81 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -36,6 +36,8 @@ #pragma once +#include +#include #include #include #include @@ -44,15 +46,41 @@ #include #include #include +#include #include -#include "archive/ArchiveReader.h" #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif +#include "tasks/Task.h" namespace MMCZip { -using FilterFileFunction = std::function; +using FilterFunction = std::function; + +/** + * Merge two zip files, using a filter function + */ +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr); + +/** + * Compress directory, by providing a list of files to compress + * \param zip target archive + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ +bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false); + +/** + * Compress directory, by providing a list of files to compress + * \param fileCompressed target archive file + * \param dir directory that will be compressed (to compress with relative paths) + * \param files list of files to compress + * \param followSymlinks should follow symlinks when compressing file data + * \return true for success or false for failure + */ +bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); #if defined(LAUNCHER_APPLICATION) /** @@ -60,11 +88,29 @@ using FilterFileFunction = std::function; */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); #endif +/** + * Find a single file in archive by file name (not path) + * + * \param ignore_paths paths to skip when recursing the search + * + * \return the path prefix where the file is + */ +QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, const QString& root = QString("")); + +/** + * Find a multiple files of the same name in archive by file name + * If a file is found in a path, no deeper paths are searched + * + * \return true if anything was found + */ +bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString()); /** * Extract a subdirectory from an archive */ -std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target); +std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target); + +bool extractRelFile(QuaZip* zip, const QString& file, const QString& target); /** * Extract a whole archive. @@ -103,5 +149,91 @@ bool extractFile(QString fileCompressed, QString file, QString dir); * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); + +#if defined(LAUNCHER_APPLICATION) +class ExportToZipTask : public Task { + Q_OBJECT + public: + ExportToZipTask(QString outputPath, + QDir dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) + : m_output_path(outputPath) + , m_output(outputPath) + , m_dir(dir) + , m_files(files) + , m_destination_prefix(destinationPrefix) + , m_follow_symlinks(followSymlinks) + { + setAbortable(true); + m_output.setUtf8Enabled(utf8Enabled); + }; + ExportToZipTask(QString outputPath, + QString dir, + QFileInfoList files, + QString destinationPrefix = "", + bool followSymlinks = false, + bool utf8Enabled = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; + + virtual ~ExportToZipTask() = default; + + void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } + void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult exportZip(); + void finish(); + + private: + QString m_output_path; + QuaZip m_output; + QDir m_dir; + QFileInfoList m_files; + QString m_destination_prefix; + bool m_follow_symlinks; + QStringList m_exclude_files; + QHash m_extra_files; + + QFuture m_build_zip_future; + QFutureWatcher m_build_zip_watcher; +}; + +class ExtractZipTask : public Task { + Q_OBJECT + public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) + {} + ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") + : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + std::shared_ptr m_input; + QDir m_output_dir; + QString m_subdirectory; + + QFuture m_zip_future; + QFutureWatcher m_zip_watcher; +}; +#endif } // namespace MMCZip diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index 97db598de..0ba9c5ac8 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, RET_DEF) \ +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ static RET_TYPE NAME() \ { \ - RET_TYPE ret = RET_DEF; \ + RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ return ret; \ } -#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE) \ +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \ { \ - RET_TYPE ret = RET_DEF; \ + RET_TYPE ret; \ 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, RET_DEF, PARAM_1_TYPE, PARAM_2_TYPE) \ +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ { \ - RET_TYPE ret = RET_DEF; \ + RET_TYPE ret; \ 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, -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) + 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) // NOTE: Every function returns something non-void to simplify the macros. private slots: diff --git a/launcher/LibraryUtils.cpp b/launcher/MangoHud.cpp similarity index 92% rename from launcher/LibraryUtils.cpp rename to launcher/MangoHud.cpp index 4ac038114..29a7c63d9 100644 --- a/launcher/LibraryUtils.cpp +++ b/launcher/MangoHud.cpp @@ -25,7 +25,7 @@ #include "FileSystem.h" #include "Json.h" -#include "LibraryUtils.h" +#include "MangoHud.h" #ifdef __GLIBC__ #ifndef _GNU_SOURCE @@ -36,9 +36,9 @@ #include #endif -namespace LibraryUtils { +namespace MangoHud { -QString findMangoHud() +QString getLibraryString() { /** * Guess MangoHud install location by searching for vulkan layers in this order: @@ -111,8 +111,8 @@ QString findMangoHud() try { auto conf = Json::requireDocument(filePath, vkLayer); auto confObject = Json::requireObject(conf, vkLayer); - auto layer = confObject["layer"].toObject(); - QString libraryName = layer["library_path"].toString(); + auto layer = Json::ensureObject(confObject, "layer"); + QString libraryName = Json::ensureString(layer, "library_path"); if (libraryName.isEmpty()) { continue; @@ -123,7 +123,7 @@ QString findMangoHud() #ifdef __GLIBC__ // Check whether mangohud is usable on a glibc based system - QString libraryPath = find(libraryName); + QString libraryPath = findLibrary(libraryName); if (!libraryPath.isEmpty()) { return libraryPath; } @@ -138,7 +138,7 @@ QString findMangoHud() return {}; } -QString find(QString libName) +QString findLibrary(QString libName) { #ifdef __GLIBC__ const char* library = libName.toLocal8Bit().constData(); @@ -161,11 +161,11 @@ QString find(QString libName) dlclose(handle); return fullPath; #else - qWarning() << "LibraryUtils::find is not implemented on this platform"; + qWarning() << "MangoHud::findLibrary is not implemented on this platform"; return {}; #endif } -} // namespace LibraryUtils +} // namespace MangoHud #ifdef UNDEF_GNU_SOURCE #undef _GNU_SOURCE diff --git a/launcher/LibraryUtils.h b/launcher/MangoHud.h similarity index 87% rename from launcher/LibraryUtils.h rename to launcher/MangoHud.h index 6832a9627..5361999b4 100644 --- a/launcher/LibraryUtils.h +++ b/launcher/MangoHud.h @@ -21,9 +21,9 @@ #include #include -namespace LibraryUtils { +namespace MangoHud { -QString findMangoHud(); +QString getLibraryString(); -QString find(QString libName); -} // namespace LibraryUtils +QString findLibrary(QString libName); +} // namespace MangoHud diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp index 6f6d34828..426067bf6 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 57a2e5437..f91a016bd 100644 --- a/launcher/Markdown.h +++ b/launcher/Markdown.h @@ -21,4 +21,4 @@ #include #include -QString markdownToHTML(const QString& markdown); +QString markdownToHTML(const QString& markdown); \ No newline at end of file diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index e5f4eb19a..116e70c4b 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -1,23 +1,20 @@ #include "MessageLevel.h" -MessageLevel MessageLevel::fromName(const QString& levelName) +MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) { - QString name = levelName.toUpper(); - if (name == "LAUNCHER") + if (levelName == "Launcher") return MessageLevel::Launcher; - else if (name == "TRACE") - return MessageLevel::Trace; - else if (name == "DEBUG") + else if (levelName == "Debug") return MessageLevel::Debug; - else if (name == "INFO") + else if (levelName == "Info") return MessageLevel::Info; - else if (name == "MESSAGE") + else if (levelName == "Message") return MessageLevel::Message; - else if (name == "WARNING" || name == "WARN") + else if (levelName == "Warning") return MessageLevel::Warning; - else if (name == "ERROR" || name == "CRITICAL") + else if (levelName == "Error") return MessageLevel::Error; - else if (name == "FATAL") + else if (levelName == "Fatal") return MessageLevel::Fatal; // Skip PrePost, it's not exposed to !![]! // Also skip StdErr and StdOut @@ -25,47 +22,12 @@ MessageLevel MessageLevel::fromName(const QString& levelName) return MessageLevel::Unknown; } -MessageLevel MessageLevel::fromQtMsgType(const QtMsgType& type) -{ - switch (type) { - case QtDebugMsg: - return MessageLevel::Debug; - case QtInfoMsg: - return MessageLevel::Info; - case QtWarningMsg: - return MessageLevel::Warning; - case QtCriticalMsg: - return MessageLevel::Error; - case QtFatalMsg: - return MessageLevel::Fatal; - default: - return MessageLevel::Unknown; - } -} - -/* Get message level from a line. Line is modified if it was successful. */ -MessageLevel MessageLevel::takeFromLine(QString& line) +MessageLevel::Enum MessageLevel::fromLine(QString& line) { // Level prefix int endmark = line.indexOf("]!"); if (line.startsWith("!![") && endmark != -1) { - auto level = MessageLevel::fromName(line.left(endmark).mid(3)); - line = line.mid(endmark + 2); - return level; - } - return MessageLevel::Unknown; -} - -/* Get message level from a line from the launcher log. Line is modified if it was successful. */ -MessageLevel MessageLevel::takeFromLauncherLine(QString& line) -{ - // Level prefix - int startMark = 0; - while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) - ++startMark; - int endmark = line.indexOf(":"); - if (startMark < line.size() && endmark != -1) { - auto level = MessageLevel::fromName(line.left(endmark).mid(startMark)); + auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); line = line.mid(endmark + 2); return level; } diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index cff098664..fd12583f2 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,43 +1,26 @@ #pragma once #include -#include /** * @brief the MessageLevel Enum * defines what level a log message is */ -struct MessageLevel { - enum class Enum { - Unknown, /**< No idea what this is or where it came from */ - StdOut, /**< Undetermined stderr messages */ - StdErr, /**< Undetermined stdout messages */ - Launcher, /**< Launcher Messages */ - Trace, /**< Trace Messages */ - Debug, /**< Debug Messages */ - Info, /**< Info Messages */ - Message, /**< Standard Messages */ - Warning, /**< Warnings */ - Error, /**< Errors */ - Fatal, /**< Fatal Errors */ - }; - using enum Enum; - constexpr MessageLevel(Enum e = Unknown) : m_type(e) {} - static MessageLevel fromName(const QString& type); - static MessageLevel fromQtMsgType(const QtMsgType& type); - static MessageLevel fromLine(const QString& line); - inline bool isValid() const { return m_type != Unknown; } - std::strong_ordering operator<=>(const MessageLevel& other) const = default; - std::strong_ordering operator<=>(const MessageLevel::Enum& other) const { return m_type <=> other; } - explicit operator int() const { return static_cast(m_type); } - explicit operator MessageLevel::Enum() { return m_type; } - - /* Get message level from a line. Line is modified if it was successful. */ - static MessageLevel takeFromLine(QString& line); - - /* Get message level from a line from the launcher log. Line is modified if it was successful. */ - static MessageLevel takeFromLauncherLine(QString& line); - - private: - Enum m_type; +namespace MessageLevel { +enum Enum { + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + Launcher, /**< Launcher Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ }; +MessageLevel::Enum getLevel(const QString& levelName); + +/* Get message level from a line. Line is modified if it was successful. */ +MessageLevel::Enum fromLine(QString& line); +} // namespace MessageLevel diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 1c2425e16..3d01c9d33 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -41,8 +41,8 @@ class NullInstance : public BaseInstance { Q_OBJECT public: - NullInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) - : BaseInstance(globalSettings, std::move(settings), rootDir) + NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + : BaseInstance(globalSettings, settings, rootDir) { setVersionBroken(true); } @@ -52,12 +52,13 @@ class NullInstance : public BaseInstance { QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; - LaunchTask* createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } + shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } - QStringList getLogFileSearchPaths() override { return {}; } + IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } + QString getLogFileRoot() override { return instanceRoot(); } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } @@ -70,7 +71,7 @@ class NullInstance : public BaseInstance { return out; } QString modsRoot() const override { return QString(); } - void updateRuntimeContext() override + void updateRuntimeContext() { // NOOP } diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h index 533195e94..ba6154ad8 100644 --- a/launcher/PSaveFile.h +++ b/launcher/PSaveFile.h @@ -67,5 +67,5 @@ class PSaveFile : public QSaveFile { QString m_absoluteFilePath; }; #else -using PSaveFile = QSaveFile; -#endif +#define PSaveFile QSaveFile +#endif \ No newline at end of file diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index 88c17c0b2..a1c64b433 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -33,7 +33,7 @@ class shared_qobject_ptr : public QSharedPointer { {} void reset() { QSharedPointer::reset(); } - void reset(T* other) + void reset(T*&& other) { shared_qobject_ptr t(other); this->swap(t); diff --git a/launcher/QVariantUtils.h b/launcher/QVariantUtils.h index 23fe82573..91f2ad29c 100644 --- a/launcher/QVariantUtils.h +++ b/launcher/QVariantUtils.h @@ -66,4 +66,4 @@ inline QVariant fromList(QList val) return variantList; } -} // namespace QVariantUtils +} // namespace QVariantUtils \ No newline at end of file diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index b0137fb5c..8b28a03f1 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -1,6 +1,7 @@ #include "RecursiveFileSystemWatcher.h" #include +#include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { @@ -78,7 +79,7 @@ QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) } for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) { auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); - if (m_matcher(relPath)) { + if (m_matcher->matches(relPath)) { ret.append(relPath); } } diff --git a/launcher/RecursiveFileSystemWatcher.h b/launcher/RecursiveFileSystemWatcher.h index 0a71e64c2..7f96f5cd0 100644 --- a/launcher/RecursiveFileSystemWatcher.h +++ b/launcher/RecursiveFileSystemWatcher.h @@ -2,7 +2,7 @@ #include #include -#include "Filter.h" +#include "pathmatcher/IPathMatcher.h" class RecursiveFileSystemWatcher : public QObject { Q_OBJECT @@ -16,7 +16,7 @@ class RecursiveFileSystemWatcher : public QObject { void setWatchFiles(bool watchFiles); bool watchFiles() const { return m_watchFiles; } - void setMatcher(Filter matcher) { m_matcher = std::move(matcher); } + void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; } QStringList files() const { return m_files; } @@ -32,7 +32,7 @@ class RecursiveFileSystemWatcher : public QObject { QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; - Filter m_matcher; + IPathMatcher::Ptr m_matcher; QFileSystemWatcher* m_watcher; diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index bb44d2495..6f5b9a189 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -19,54 +19,25 @@ #include "ResourceDownloadTask.h" -#include - #include "Application.h" -#include "FileSystem.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "minecraft/mod/ModFolderModel.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, - ResourceFolderModel* packs, - bool isIndexed, - QString downloadReason) - : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) + const std::shared_ptr packs, + bool is_indexed, + QString custom_target_folder) + : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder) { - if (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); + if (auto model = dynamic_cast(m_pack_model.get()); model && is_indexed) { + m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version)); + connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource); addTask(m_update_task); } @@ -74,9 +45,17 @@ 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()), - Net::Download::Option::NoOptions, - createModrinthMeta(m_pack_model->instance(), std::move(downloadReason))); + QDir dir{ m_pack_model->dir() }; + { + // FIXME: Make this more generic. May require adding additional info to IndexedVersion, + // or adquiring a reference to the base instance. + if (!m_custom_target_folder.isEmpty()) { + dir.cdUp(); + dir.cd(m_custom_target_folder); + } + } + + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: @@ -110,34 +89,20 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, void ResourceDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); - auto oldName = std::get<0>(to_delete); - auto oldFilename = std::get<1>(to_delete); - - 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) != 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) { - emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); - } - } + auto name = std::get<0>(to_delete); + auto filename = std::get<1>(to_delete); + if (!name.isEmpty() && filename != m_pack_version.fileName) { + if (auto model = dynamic_cast(m_pack_model.get()); model) + model->uninstallMod(filename, true); + else + m_pack_model->uninstallResource(filename); } } void ResourceDownloadTask::downloadFailed(QString reason) { + emitFailed(reason); m_filesNetJob.reset(); - emitFailed(std::move(reason)); } void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) @@ -147,7 +112,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(const QString& name, const QString& filename) +void ResourceDownloadTask::hasOldResource(QString name, QString filename) { to_delete = { name, filename }; } diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 84324e99a..f686e819a 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -22,7 +22,7 @@ #include "net/NetJob.h" #include "tasks/SequentialTask.h" -#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "modplatform/ModIndex.h" class ResourceFolderModel; @@ -32,10 +32,11 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - ResourceFolderModel* packs, - bool isIndexed = true, - QString downloadReason = "standalone"); + std::shared_ptr packs, + bool is_indexed = true, + QString custom_target_folder = {}); const QString& getFilename() const { return m_pack_version.fileName; } + const QString& getCustomPath() const { return m_custom_target_folder; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } @@ -45,10 +46,11 @@ class ResourceDownloadTask : public SequentialTask { private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; - ResourceFolderModel* m_pack_model; + const std::shared_ptr m_pack_model; + QString m_custom_target_folder; NetJob::Ptr m_filesNetJob; - LocalResourceUpdateTask::Ptr m_update_task; + LocalModUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); void downloadFailed(QString reason); @@ -57,5 +59,5 @@ class ResourceDownloadTask : public SequentialTask { std::tuple to_delete{ "", "" }; private slots: - void hasOldResource(const QString& name, const QString& filename); + void hasOldResource(QString name, QString filename); }; diff --git a/launcher/RuntimeContext.h b/launcher/RuntimeContext.h index 84a56a892..85304a5bc 100644 --- a/launcher/RuntimeContext.h +++ b/launcher/RuntimeContext.h @@ -41,7 +41,7 @@ struct RuntimeContext { return javaRealArchitecture; } - void updateFromInstanceSettings(SettingsObject* instanceSettings) + void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index a79cfb5c5..edda9f247 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -35,6 +35,7 @@ */ #include "StringUtils.h" +#include #include #include @@ -52,7 +53,7 @@ static inline QChar getNextChar(const QString& s, int location) int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; - while (l1 <= s1.size() && l2 <= s2.size()) { + while (l1 <= s1.count() && l2 <= s2.count()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) @@ -212,10 +213,11 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular return qMakePair(left, right); } +static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); + QString StringUtils::htmlListPatch(QString htmlStr) { - static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); - int pos = htmlStr.indexOf(s_ulMatcher); + int pos = htmlStr.indexOf(ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index @@ -228,7 +230,7 @@ QString StringUtils::htmlListPatch(QString htmlStr) if (textBetween.isEmpty()) htmlStr.insert(pos, "
"); - pos = htmlStr.indexOf(s_ulMatcher, pos); + pos = htmlStr.indexOf(ulMatcher, pos); } return htmlStr; -} +} \ No newline at end of file diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp index d02b1d854..cfcf63805 100644 --- a/launcher/SysInfo.cpp +++ b/launcher/SysInfo.cpp @@ -1,54 +1,20 @@ - -// 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 - -#include "HardwareInfo.h" - +#include "sys.h" #ifdef Q_OS_MACOS #include +#endif +#include +#include +#include +#include +#ifdef Q_OS_MACOS bool rosettaDetect() { int ret = 0; size_t size = sizeof(ret); - if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { return false; } return ret == 1; @@ -85,13 +51,18 @@ QString useQTForArch() return QSysInfo::currentCpuArchitecture(); } -int defaultMaxJvmMem() +int suitableMaxMem() { + float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; + int maxMemoryAlloc; + // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB - if (const uint64_t totalRAM = HardwareInfo::totalRamMiB(); totalRAM < (4096 * 1.5)) - return totalRAM / 1.5; + if (totalRAM < (4096 * 1.5)) + maxMemoryAlloc = (int)(totalRAM / 1.5); else - return 4096; + maxMemoryAlloc = 4096; + + return maxMemoryAlloc; } QString getSupportedJavaArchitecture() diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h index da23c5be1..f3688d60d 100644 --- a/launcher/SysInfo.h +++ b/launcher/SysInfo.h @@ -1,12 +1,8 @@ -#pragma once - -#include - #include namespace SysInfo { QString currentSystem(); QString useQTForArch(); QString getSupportedJavaArchitecture(); -int defaultMaxJvmMem(); +int suitableMaxMem(); } // namespace SysInfo diff --git a/launcher/Untar.cpp b/launcher/Untar.cpp new file mode 100644 index 000000000..f1963e7aa --- /dev/null +++ b/launcher/Untar.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Untar.h" +#include +#include +#include +#include +#include +#include "FileSystem.h" + +// adaptation of the: +// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c +// - https://en.wikipedia.org/wiki/Tar_(computing) +// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp + +#define BLOCKSIZE 512 +#define SHORTNAMESIZE 100 + +enum class TypeFlag : char { + Regular = '0', // regular file + ARegular = 0, // regular file + Link = '1', // link + Symlink = '2', // reserved + Character = '3', // character special + Block = '4', // block special + Directory = '5', // directory + FIFO = '6', // FIFO special + Contiguous = '7', // reserved + // Posix stuff + GlobalPosixHeader = 'g', + ExtendedPosixHeader = 'x', + // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) + // GNU + GNULongLink = 'K', /* long link name */ + GNULongName = 'L', /* long file name */ +}; + +// struct Header { /* byte offset */ +// char name[100]; /* 0 */ +// char mode[8]; /* 100 */ +// char uid[8]; /* 108 */ +// char gid[8]; /* 116 */ +// char size[12]; /* 124 */ +// char mtime[12]; /* 136 */ +// char chksum[8]; /* 148 */ +// TypeFlag typeflag; /* 156 */ +// char linkname[100]; /* 157 */ +// char magic[6]; /* 257 */ +// char version[2]; /* 263 */ +// char uname[32]; /* 265 */ +// char gname[32]; /* 297 */ +// char devmajor[8]; /* 329 */ +// char devminor[8]; /* 337 */ +// char prefix[155]; /* 345 */ +// /* 500 */ +// }; + +bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) +{ + qint64 n = 0; + size--; // ignore trailing null + if (size < 0) { + qCritical() << "The filename size is negative"; + return false; + } + longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE + for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) { + n = in->read(longlink.data() + offset, BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected for the name"; + return false; + } + } + longlink.truncate(qstrlen(longlink.constData())); + return true; +} + +int getOctal(char* buffer, int maxlenght, bool* ok) +{ + return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); +} + +QString decodeName(char* name) +{ + return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); +} +bool Tar::extract(QIODevice* in, QString dst) +{ + char buffer[BLOCKSIZE]; + QString name, symlink, firstFolderName; + bool doNotReset = false, ok; + while (true) { + auto n = in->read(buffer, BLOCKSIZE); + if (n != BLOCKSIZE) { // allways expect complete blocks + qCritical() << "The expected blocksize was not respected"; + return false; + } + if (buffer[0] == 0) { // end of archive + return true; + } + int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions + if (!ok) { + qCritical() << "The file mode can't be read"; + return false; + } + // there are names that are exactly 100 bytes long + // and neither longlink nor \0 terminated (bug:101472) + + if (name.isEmpty()) { + name = decodeName(buffer); + if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) { + name = name.mid(firstFolderName.size()); + } + } + if (symlink.isEmpty()) + symlink = decodeName(buffer); + qint64 size = getOctal(buffer + 124, 12, &ok); + if (!ok) { + qCritical() << "The file size can't be read"; + return false; + } + switch (TypeFlag(buffer[156])) { + case TypeFlag::Regular: + /* fallthrough */ + case TypeFlag::ARegular: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::ensureFilePathExists(fileName)) { + qCritical() << "Can't ensure the file path to exist: " << fileName; + return false; + } + QFile out(fileName); + if (!out.open(QFile::WriteOnly)) { + qCritical() << "Can't open file:" << fileName; + return false; + } + out.setPermissions(QFile::Permissions(mode)); + while (size > 0) { + QByteArray tmp(BLOCKSIZE, 0); + n = in->read(tmp.data(), BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected when reading file"; + return false; + } + tmp.truncate(qMin(qint64(BLOCKSIZE), size)); + out.write(tmp); + size -= BLOCKSIZE; + } + break; + } + case TypeFlag::Directory: { + if (firstFolderName.isEmpty()) { + firstFolderName = name; + break; + } + auto folderPath = FS::PathCombine(dst, name); + if (!FS::ensureFolderPathExists(folderPath)) { + qCritical() << "Can't ensure that folder exists: " << folderPath; + return false; + } + break; + } + case TypeFlag::GNULongLink: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + symlink = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long link"; + return false; + } + break; + } + case TypeFlag::GNULongName: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + name = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long name"; + return false; + } + break; + } + case TypeFlag::Link: + /* fallthrough */ + case TypeFlag::Symlink: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks + qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); + return false; + } + FS::ensureFilePathExists(fileName); + QFile::setPermissions(fileName, QFile::Permissions(mode)); + break; + } + case TypeFlag::Character: + /* fallthrough */ + case TypeFlag::Block: + /* fallthrough */ + case TypeFlag::FIFO: + /* fallthrough */ + case TypeFlag::Contiguous: + /* fallthrough */ + case TypeFlag::GlobalPosixHeader: + /* fallthrough */ + case TypeFlag::ExtendedPosixHeader: + /* fallthrough */ + default: + break; + } + if (!doNotReset) { + name.truncate(0); + symlink.truncate(0); + } + doNotReset = false; + } + return true; +} + +bool GZTar::extract(QString src, QString dst) +{ + QuaGzipFile a(src); + if (!a.open(QIODevice::ReadOnly)) { + qCritical() << "Can't open tar file:" << src; + return false; + } + return Tar::extract(&a, dst); +} \ No newline at end of file diff --git a/launcher/logs/AnonymizeLog.h b/launcher/Untar.h similarity index 78% rename from launcher/logs/AnonymizeLog.h rename to launcher/Untar.h index 215d1e468..50e3a16e3 100644 --- a/launcher/logs/AnonymizeLog.h +++ b/launcher/Untar.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,7 +21,6 @@ * 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 * @@ -34,7 +33,14 @@ * limitations under the License. */ #pragma once +#include -#include +// this is a hack used for the java downloader (feel free to remove it in favor of a library) +// both extract functions will extract the first folder inside dest(disregarding the prefix) +namespace Tar { +bool extract(QIODevice* in, QString dst); +} -void anonymizeLog(QString& log); +namespace GZTar { +bool extract(QString src, QString dst); +} \ No newline at end of file diff --git a/launcher/Usable.h b/launcher/Usable.h index 8cef29868..b0ecd4018 100644 --- a/launcher/Usable.h +++ b/launcher/Usable.h @@ -36,7 +36,7 @@ class Usable { */ class UseLock { public: - UseLock(Usable* usable) : m_usable(usable) + UseLock(shared_qobject_ptr 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: - Usable* m_usable; + shared_qobject_ptr m_usable; }; diff --git a/launcher/Version.cpp b/launcher/Version.cpp index d5496ce1c..2edb17e72 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,42 +1,125 @@ -// 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 #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 = [&](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)); +} /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) { - const QDebugStateSaver saver(debug); + QDebugStateSaver saver(debug); debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; bool first = true; - for (const auto& s : v.m_sections) { - if (!first) { + for (auto s : v.m_sections) { + if (!first) debug.nospace() << ", "; - } - debug.nospace() << s.value; + debug.nospace() << s.m_fullString; first = false; } @@ -44,114 +127,3 @@ 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 c0f70f487..b06e256aa 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -3,7 +3,6 @@ * 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 @@ -16,6 +15,23 @@ * * 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 @@ -25,36 +41,123 @@ #include #include -// this implements the FlexVer -// https://git.sleeping.town/exa/FlexVer +class QUrl; + class Version { public: - Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions) + Version(QString str); Version() = default; - 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; - }; + 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: - void parse(); - - public: QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); - bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; } - std::strong_ordering operator<=>(const Version& other) const; + 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; + } + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto numPart = QStringView{ m_fullString }.left(cutoff); +#else + auto numPart = m_fullString.leftRef(cutoff); +#endif + + if (!numPart.isEmpty()) { + m_isNull = false; + m_numPart = numPart.toInt(); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto stringPart = QStringView{ m_fullString }.mid(cutoff); +#else + auto stringPart = m_fullString.midRef(cutoff); +#endif + + if (!stringPart.isEmpty()) { + m_isNull = false; + m_stringPart = stringPart.toString(); + } + } + + explicit Section() = default; + + bool m_isNull = true; + + int m_numPart = 0; + QString m_stringPart; + + QString m_fullString; + + [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } + [[nodiscard]] 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); } + }; private: QString m_string; QList
m_sections; -}; \ No newline at end of file + + void parse(); +}; diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index aaab7e8e0..12a82f73d 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -37,9 +37,9 @@ #include "VersionProxyModel.h" #include #include -#include #include #include +#include "Application.h" class VersionFilterModel : public QSortFilterProxyModel { Q_OBJECT @@ -63,7 +63,7 @@ class VersionFilterModel : public QSortFilterProxyModel { for (auto it = filters.begin(); it != filters.end(); ++it) { auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); - if (!it.value()(match)) { + if (!it.value()->accepts(match)) { return false; } } @@ -193,36 +193,43 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const if (value.toBool()) { return tr("Recommended"); } else if (hasLatest) { - auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (latest.toBool()) { + auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (value.toBool()) { return tr("Latest"); } } + } else { + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } - return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } case Qt::DecorationRole: { - if (column == Name && hasRecommended) { - auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); - if (recommenced.toBool()) { - return QIcon::fromTheme("star"); - } else if (hasLatest) { - auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (latest.toBool()) { - return QIcon::fromTheme("bug"); + switch (column) { + case Name: { + if (hasRecommended) { + auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (recommenced.toBool()) { + return APPLICATION->getThemedIcon("star"); + } else if (hasLatest) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { + return APPLICATION->getThemedIcon("bug"); + } + } + QPixmap pixmap; + QPixmapCache::find("placeholder", &pixmap); + if (!pixmap) { + QPixmap px(16, 16); + px.fill(Qt::transparent); + QPixmapCache::insert("placeholder", px); + return px; + } + return pixmap; } } - QPixmap pixmap; - QPixmapCache::find("placeholder", &pixmap); - if (!pixmap) { - QPixmap px(16, 16); - px.fill(Qt::transparent); - QPixmapCache::insert("placeholder", px); - return px; + default: { + return QVariant(); } - return pixmap; } - return QVariant(); } default: { if (roles.contains((BaseVersionList::ModelRoles)role)) { @@ -294,6 +301,7 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); + beginResetModel(); m_columns.clear(); if (!replacing) { @@ -340,6 +348,8 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) hasLatest = true; } filterModel->setSourceModel(replacing); + + endResetModel(); } QModelIndex VersionProxyModel::getRecommended() const @@ -379,9 +389,9 @@ void VersionProxyModel::clearFilters() filterModel->filterChanged(); } -void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) +void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter* f) { - m_filters[column] = std::move(f); + m_filters[column].reset(f); filterModel->filterChanged(); } diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index ddd5d2458..7965af0ad 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -10,7 +10,7 @@ class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; - using FilterMap = QHash; + using FilterMap = QHash>; public: VersionProxyModel(QObject* parent = 0); @@ -28,7 +28,7 @@ class VersionProxyModel : public QAbstractProxyModel { const FilterMap& filters() const; const QString& search() const; - void setFilter(BaseVersionList::ModelRoles column, Filter filter); + void setFilter(BaseVersionList::ModelRoles column, Filter* filter); void setSearch(const QString& search); void clearFilters(); QModelIndex getRecommended() const; diff --git a/launcher/console/WindowsConsole.cpp b/launcher/WindowsConsole.cpp similarity index 70% rename from launcher/console/WindowsConsole.cpp rename to launcher/WindowsConsole.cpp index e12183624..83cad5afa 100644 --- a/launcher/console/WindowsConsole.cpp +++ b/launcher/WindowsConsole.cpp @@ -16,24 +16,15 @@ * */ -#include "WindowsConsole.h" -#include - #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include - -#include #include -#include #include #include -#include +#include #include -namespace console { - void RedirectHandle(DWORD handle, FILE* stream, const char* mode) { HANDLE stdHandle = GetStdHandle(handle); @@ -135,57 +126,3 @@ bool AttachWindowsConsole() return false; } - -std::error_code EnableAnsiSupport() -{ - // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew - // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected - HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0); - if (console_handle == INVALID_HANDLE_VALUE) { - return std::error_code(GetLastError(), std::system_category()); - } - - // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode - DWORD console_mode; - if (0 == GetConsoleMode(console_handle, &console_mode)) { - return std::error_code(GetLastError(), std::system_category()); - } - - // VT processing not already enabled? - if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) { - // https://docs.microsoft.com/en-us/windows/console/setconsolemode - if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { - return std::error_code(GetLastError(), std::system_category()); - } - } - - return {}; -} - -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/LaunchMode.h b/launcher/WindowsConsole.h similarity index 78% rename from launcher/LaunchMode.h rename to launcher/WindowsConsole.h index 45cfe50ce..ab53864b4 100644 --- a/launcher/LaunchMode.h +++ b/launcher/WindowsConsole.h @@ -1,7 +1,9 @@ +// // SPDX-License-Identifier: GPL-3.0-only + /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2026 Octol1ttle + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,12 +16,10 @@ * * 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, -}; +void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); +bool AttachWindowsConsole(); diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp deleted file mode 100644 index 764063de3..000000000 --- a/launcher/archive/ArchiveReader.cpp +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 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 . - * - * Additional note: Portions of this file are released into the public domain - * under LicenseRef-PublicDomain. - */ -#include "ArchiveReader.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace MMCZip { -QStringList ArchiveReader::getFiles() -{ - return m_fileNames; -} - -bool ArchiveReader::collectFiles(bool onlyFiles) -{ - return parse([this, onlyFiles](File* f) { - 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 decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname); -} - -QByteArray ArchiveReader::File::readAll(int* outStatus) -{ - QByteArray data; - const void* buff = nullptr; - size_t size = 0; - la_int64_t offset = 0; - - 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()); - } - if (outStatus) { - *outStatus = status; - } - return data; -} - -QDateTime ArchiveReader::File::dateTime() -{ - auto mtime = archive_entry_mtime(m_entry); - auto mtime_nsec = archive_entry_mtime_nsec(m_entry); - auto dt = QDateTime::fromSecsSinceEpoch(mtime); - return dt.addMSecs(mtime_nsec / 1e6); -} - -int ArchiveReader::File::readNextHeader() -{ - return archive_read_next_header(m_archive.get(), &m_entry); -} - -auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr -{ - auto f = std::make_unique(); - auto* a = f->m_archive.get(); - archive_read_support_format_all(a); - archive_read_support_filter_all(a); - auto fileName = m_archivePath.toStdWString(); - if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { - qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a); - return nullptr; - } - - while (f->readNextHeader() == ARCHIVE_OK) { - if (f->filename() == filename) { - return f; - } - f->skip(); - } - - archive_read_close(a); - return nullptr; -} - -static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) -{ - 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_OK) { - qCritical() << "Failed reading data block:" << archive_error_string(ar); - return (r); - } - if (notBlock) { - r = archive_write_data(aw, buff, size); - } else { - r = archive_write_data_block(aw, buff, size, offset); - } - if (r < ARCHIVE_OK) { - qCritical() << "Failed writing data block:" << archive_error_string(aw); - return (r); - } - } -} - -static bool willEscapeRoot(const QDir& root, archive_entry* 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)); - entry = entryClone.get(); - auto nameUtf8 = targetFileName.toUtf8(); - archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); - } - if (root.has_value() && willEscapeRoot(root.value(), entry)) { - qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root"; - return false; - } - 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) { - qCritical() << "Failed reading data block:" << archive_error_string(out); - } - if (r < ARCHIVE_WARN) { - return false; - } - } - auto r = archive_write_finish_entry(out); - if (r < ARCHIVE_OK) { - qCritical() << "Failed to finish writing entry:" << archive_error_string(out); - } - return (r >= ARCHIVE_WARN); -} - -bool ArchiveReader::parse(const std::function& doStuff) -{ - auto f = std::make_unique(); - auto* a = f->m_archive.get(); - archive_read_support_format_all(a); - archive_read_support_filter_all(a); - auto fileName = m_archivePath.toStdWString(); - if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { - qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error(); - return false; - } - - bool breakControl = false; - while (f->readNextHeader() == ARCHIVE_OK) { - if (f && !doStuff(f.get(), breakControl)) { - qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error(); - return false; - } - if (breakControl) { - break; - } - } - - archive_read_close(a); - return true; -} - -bool ArchiveReader::parse(const std::function& doStuff) -{ - return parse([doStuff](File* f, bool&) { return doStuff(f); }); -} - -bool ArchiveReader::File::isFile() -{ - return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG; -} -bool ArchiveReader::File::skip() -{ - return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK; -} -const char* ArchiveReader::File::error() -{ - return archive_error_string(m_archive.get()); -} -QString ArchiveReader::getZipName() -{ - return m_archivePath; -} - -bool ArchiveReader::exists(const QString& filePath) const -{ - if (filePath == QLatin1String("/") || filePath.isEmpty()) { - return true; - } - // Normalize input path (remove trailing slash, if any) - QString normalizedPath = QDir::cleanPath(filePath); - if (normalizedPath.startsWith('/')) { - normalizedPath.remove(0, 1); - } - if (normalizedPath == QLatin1String(".")) { - return true; - } - if (normalizedPath == QLatin1String("..")) { - return false; // root only - } - - // Check for exact file match - 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)) { - return true; - } - } - - return false; -} - -ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {} -} // namespace MMCZip diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h deleted file mode 100644 index 4f11d2e06..000000000 --- a/launcher/archive/ArchiveReader.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -struct archive; -struct archive_entry; -namespace MMCZip { -class ArchiveReader { - public: - using ArchivePtr = std::unique_ptr; - explicit ArchiveReader(QString fileName) : m_archivePath(std::move(fileName)) {} - virtual ~ArchiveReader() = default; - - QStringList getFiles(); - QString getZipName(); - bool collectFiles(bool onlyFiles = true); - bool exists(const QString& filePath) const; - - class File { - public: - File(); - virtual ~File() = default; - - QString filename(); - bool isFile(); - QDateTime dateTime(); - const char* error(); - - QByteArray readAll(int* outStatus = nullptr); - bool skip(); - 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(); - - private: - friend ArchiveReader; - ArchivePtr m_archive; - archive_entry* m_entry; - }; - - 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; -}; -} // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.cpp b/launcher/archive/ArchiveWriter.cpp deleted file mode 100644 index 43dbe4dbd..000000000 --- a/launcher/archive/ArchiveWriter.cpp +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 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 "ArchiveWriter.h" -#include -#include -#include - -#include -#include - -#include - -#if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -// clang-format off -#include -#include -// clang-format on -#endif - -namespace MMCZip { - -ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {} - -ArchiveWriter::~ArchiveWriter() -{ - close(); -} - -bool ArchiveWriter::open() -{ - if (m_filename.isEmpty()) { - qCritical() << "Archive m_filename not set."; - return false; - } - - m_archive = archive_write_new(); - if (!m_archive) { - qCritical() << "Archive not initialized."; - return false; - } - - auto format = m_format.toUtf8(); - archive_write_set_format_by_name(m_archive, format.constData()); - - if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) { - qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); - return false; - } - - auto archiveNameW = m_filename.toStdWString(); - if (archive_write_open_filename_w(m_archive, archiveNameW.data()) != ARCHIVE_OK) { - qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); - return false; - } - - return true; -} - -bool ArchiveWriter::close() -{ - bool success = true; - if (m_archive) { - if (archive_write_close(m_archive) != ARCHIVE_OK) { - qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive); - success = false; - } - if (archive_write_free(m_archive) != ARCHIVE_OK) { - qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive); - success = false; - } - m_archive = nullptr; - } - return success; -} - -bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) -{ - QFileInfo fileInfo(fileName); - if (!fileInfo.exists()) { - qCritical() << "File does not exist:" << fileInfo.filePath(); - return false; - } - - std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); - auto entry = entry_ptr.get(); - if (!entry) { - qCritical() << "Failed to create archive entry"; - return false; - } - - auto fileDestUtf8 = fileDest.toUtf8(); - archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); - -#if defined Q_OS_WIN32 - { - // Windows needs to use this method, thanks I hate it. - - auto widePath = fileInfo.absoluteFilePath().toStdWString(); - HANDLE file_handle = CreateFileW(widePath.data(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (file_handle == INVALID_HANDLE_VALUE) { - qCritical() << "Failed to stat file:" << fileInfo.filePath(); - return false; - } - - BY_HANDLE_FILE_INFORMATION file_info; - if (!GetFileInformationByHandle(file_handle, &file_info)) { - qCritical() << "Failed to stat file:" << fileInfo.filePath(); - CloseHandle(file_handle); - return false; - } - - archive_entry_copy_bhfi(entry, &file_info); - CloseHandle(file_handle); - } -#else - { - // this only works for multibyte encoded filenames if the local is properly set, - // a wide character version doesn't seem to exist: here's hoping... - - QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8(); - const char* cpath = utf8.constData(); - struct stat st; - if (stat(cpath, &st) != 0) { - qCritical() << "Failed to stat file:" << fileInfo.filePath(); - return false; - } - - // This should handle the copying of most attributes - archive_entry_copy_stat(entry, &st); - } -#endif - - // However: - // "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]." - // - `archive_entry_stat(3)` - if (fileInfo.isSymLink()) { - archive_entry_set_filetype(entry, AE_IFLNK); - - // We also need to manually copy some attributes from the link itself, as `stat` above operates on its target - auto target = fileInfo.symLinkTarget().toUtf8(); - archive_entry_set_symlink_utf8(entry, target.constData()); - archive_entry_set_size(entry, 0); - archive_entry_set_perm(entry, fileInfo.permissions()); - } else if (fileInfo.isFile()) { - archive_entry_set_filetype(entry, AE_IFREG); - } else { - qCritical() << "Unsupported file type:" << fileInfo.filePath(); - return false; - } - - if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { - 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() << "error:" << file.errorString(); - return false; - } - - constexpr qint64 chunkSize = 8192; - QByteArray buffer; - buffer.resize(chunkSize); - - while (!file.atEnd()) { - auto bytesRead = file.read(buffer.data(), chunkSize); - if (bytesRead < 0) { - 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; - return false; - } - } - } - - return true; -} - -bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) -{ - std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); - auto entry = entry_ptr.get(); - if (!entry) { - qCritical() << "Failed to create archive entry"; - return false; - } - - auto fileDestUtf8 = fileDest.toUtf8(); - archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); - archive_entry_set_perm(entry, 0644); - - archive_entry_set_filetype(entry, AE_IFREG); - 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); - 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); - return false; - } - return true; -} - -bool ArchiveWriter::addFile(ArchiveReader::File* f) -{ - return f->writeFile(m_archive, "", true); -} - -std::unique_ptr ArchiveWriter::createDiskWriter() -{ - int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | - ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS; - - std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { - if (a) { - archive_write_close(a); - archive_write_free(a); - } - }); - - archive* ext = extPtr.get(); - archive_write_disk_set_options(ext, flags); - archive_write_disk_set_standard_lookup(ext); - - return extPtr; -} -} // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.h b/launcher/archive/ArchiveWriter.h deleted file mode 100644 index 50858b517..000000000 --- a/launcher/archive/ArchiveWriter.h +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include -#include -#include "archive/ArchiveReader.h" - -struct archive; -namespace MMCZip { - -class ArchiveWriter { - public: - ArchiveWriter(const QString& archiveName); - virtual ~ArchiveWriter(); - - bool open(); - bool close(); - - bool addFile(const QString& fileName, const QString& fileDest); - bool addFile(const QString& fileDest, const QByteArray& data); - bool addFile(ArchiveReader::File* f); - - static std::unique_ptr createDiskWriter(); - - private: - struct archive* m_archive = nullptr; - QString m_filename; - QString m_format = "zip"; -}; -} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.cpp b/launcher/archive/ExportToZipTask.cpp deleted file mode 100644 index bd3bc9032..000000000 --- a/launcher/archive/ExportToZipTask.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 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 "ExportToZipTask.h" - -#include - -#include "FileSystem.h" - -namespace MMCZip { -void ExportToZipTask::executeTask() -{ - setStatus("Adding files..."); - setProgress(0, m_files.length()); - m_buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); - connect(&m_buildZipWatcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); - m_buildZipWatcher.setFuture(m_buildZipFuture); -} - -auto ExportToZipTask::exportZip() -> ZipResult -{ - if (!m_dir.exists()) { - return ZipResult(tr("Folder doesn't exist")); - } - if (!m_output.open()) { - return ZipResult(tr("Could not create file")); - } - - for (auto fileName : m_extraFiles.keys()) { - if (m_buildZipFuture.isCanceled()) - return ZipResult(); - if (!m_output.addFile(fileName, m_extraFiles[fileName])) { - return ZipResult(tr("Could not add:") + fileName); - } - } - - for (const QFileInfo& file : m_files) { - if (m_buildZipFuture.isCanceled()) - return ZipResult(); - - auto absolute = file.absoluteFilePath(); - auto relative = m_dir.relativeFilePath(absolute); - setStatus("Compressing: " + relative); - setProgress(m_progress + 1, m_progressTotal); - if (m_followSymlinks) { - if (file.isSymLink()) - absolute = file.symLinkTarget(); - else - absolute = file.canonicalFilePath(); - } - - if (!m_excludeFiles.contains(relative) && !m_output.addFile(absolute, m_destinationPrefix + relative)) { - return ZipResult(tr("Could not read and compress %1").arg(relative)); - } - } - - if (!m_output.close()) { - return ZipResult(tr("A zip error occurred")); - } - return ZipResult(); -} - -void ExportToZipTask::finish() -{ - if (m_buildZipFuture.isCanceled()) { - FS::deletePath(m_outputPath); - emitAborted(); - } else if (auto result = m_buildZipFuture.result(); result.has_value()) { - FS::deletePath(m_outputPath); - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExportToZipTask::abort() -{ - if (m_buildZipFuture.isRunning()) { - m_buildZipFuture.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} -} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.h b/launcher/archive/ExportToZipTask.h deleted file mode 100644 index 0c8329c93..000000000 --- a/launcher/archive/ExportToZipTask.h +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include -#include -#include -#include - -#include "archive/ArchiveWriter.h" -#include "tasks/Task.h" - -namespace MMCZip { -class ExportToZipTask : public Task { - Q_OBJECT - public: - ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) - : m_outputPath(outputPath) - , m_output(outputPath) - , m_dir(dir) - , m_files(files) - , m_destinationPrefix(destinationPrefix) - , m_followSymlinks(followSymlinks) - { - setAbortable(true); - }; - ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {}; - - virtual ~ExportToZipTask() = default; - - void setExcludeFiles(QStringList excludeFiles) { m_excludeFiles = excludeFiles; } - void addExtraFile(QString fileName, QByteArray data) { m_extraFiles.insert(fileName, data); } - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult exportZip(); - void finish(); - - private: - QString m_outputPath; - ArchiveWriter m_output; - QDir m_dir; - QFileInfoList m_files; - QString m_destinationPrefix; - bool m_followSymlinks; - QStringList m_excludeFiles; - QHash m_extraFiles; - - QFuture m_buildZipFuture; - QFutureWatcher m_buildZipWatcher; -}; -} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp deleted file mode 100644 index 35dc39d90..000000000 --- a/launcher/archive/ExtractZipTask.cpp +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 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 "ExtractZipTask.h" -#include -#include "FileSystem.h" -#include "archive/ArchiveReader.h" -#include "archive/ArchiveWriter.h" - -namespace MMCZip { - -void ExtractZipTask::executeTask() -{ - m_zipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); - connect(&m_zipWatcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); - m_zipWatcher.setFuture(m_zipFuture); -} - -auto ExtractZipTask::extractZip() -> ZipResult -{ - auto target = m_outputDir.absolutePath(); - auto target_top_dir = QUrl::fromLocalFile(target); - - QStringList extracted; - - qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target; - if (!m_input.collectFiles()) { - return ZipResult(tr("Failed to enumerate files in archive")); - } - if (m_input.getFiles().isEmpty()) { - logWarning(tr("Extracting empty archives seems odd...")); - return ZipResult(); - } - - auto extPtr = ArchiveWriter::createDiskWriter(); - auto ext = extPtr.get(); - - setStatus("Extracting files..."); - setProgress(0, m_input.getFiles().count()); - ZipResult result; - auto fileName = m_input.getZipName(); - if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { - if (m_zipFuture.isCanceled()) - return false; - setProgress(m_progress + 1, m_progressTotal); - QString file_name = f->filename(); - if (!file_name.startsWith(m_subdirectory)) { - f->skip(); - return true; - } - - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); - auto original_name = relative_file_name; - setStatus("Unpacking: " + relative_file_name); - - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); - - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - - relative_file_name = relative_file_name.split('/').last(); - } - - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } - - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") - .arg(relative_file_name, target)); - return false; - } - - 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; - } - extracted.append(target_file_path); - - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - return true; - })) { - FS::removeFiles(extracted); - return result.has_value() ? result : ZipResult(tr("Failed to parse file %1").arg(fileName)); - } - return ZipResult(); -} - -void ExtractZipTask::finish() -{ - if (m_zipFuture.isCanceled()) { - emitAborted(); - } else if (auto result = m_zipFuture.result(); result.has_value()) { - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExtractZipTask::abort() -{ - if (m_zipFuture.isRunning()) { - m_zipFuture.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} - -} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.h b/launcher/archive/ExtractZipTask.h deleted file mode 100644 index 03c391aee..000000000 --- a/launcher/archive/ExtractZipTask.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -#include -#include -#include -#include "archive/ArchiveReader.h" -#include "tasks/Task.h" - -namespace MMCZip { - -class ExtractZipTask : public Task { - Q_OBJECT - public: - ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") - : m_input(input), m_outputDir(outputDir), m_subdirectory(subdirectory) - {} - virtual ~ExtractZipTask() = default; - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult extractZip(); - void finish(); - - private: - ArchiveReader m_input; - QDir m_outputDir; - QString m_subdirectory; - - QFuture m_zipFuture; - QFutureWatcher m_zipWatcher; -}; -} // namespace MMCZip diff --git a/launcher/console/Console.h b/launcher/console/Console.h deleted file mode 100644 index 7aaf83dcc..000000000 --- a/launcher/console/Console.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -#include -#if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#else -#include -#include -#endif - -namespace console { - -inline bool isConsole() -{ -#if defined Q_OS_WIN32 - DWORD procIDs[2]; - DWORD maxCount = 2; - DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount); - return result > 1; -#else - if (isatty(fileno(stdout))) { - return true; - } - return false; -#endif -} - -} // namespace console diff --git a/launcher/console/WindowsConsole.h b/launcher/console/WindowsConsole.h deleted file mode 100644 index 52102217d..000000000 --- a/launcher/console/WindowsConsole.h +++ /dev/null @@ -1,44 +0,0 @@ -// -// SPDX-License-Identifier: GPL-3.0-only - -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#pragma once - -#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 d87a1078b..bdf173ebc 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -34,11 +34,39 @@ #include +#include + +#if defined Q_OS_WIN32 +#include "WindowsConsole.h" +#endif + +// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header + +#ifdef __APPLE__ +#include // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ + +#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) +#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) +#define GHC_USE_STD_FS #include namespace fs = std::filesystem; +#endif // MacOS min version check +#endif // Other OSes version check + +#ifndef GHC_USE_STD_FS +#include +namespace fs = ghc::filesystem; +#endif FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { +#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"); @@ -76,11 +104,11 @@ void FileLinkApp::joinServer(QString server) in.setDevice(&socket); - connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); + connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; }); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); - connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) { + connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: @@ -100,11 +128,11 @@ 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(); } }); - connect(&socket, &QLocalSocket::disconnected, this, [this]() { + connect(&socket, &QLocalSocket::disconnected, this, [&]() { qDebug() << "disconnected from server, should exit"; m_status = Succeeded; exit(); @@ -221,4 +249,13 @@ 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 25fdb71fc..583d0d43a 100644 --- a/launcher/filelink/FileLink.h +++ b/launcher/filelink/FileLink.h @@ -38,6 +38,7 @@ #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 }; @@ -63,4 +64,8 @@ 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 d34844370..2a8bcb703 100644 --- a/launcher/filelink/filelink_main.cpp +++ b/launcher/filelink/filelink_main.cpp @@ -22,17 +22,8 @@ #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 ad48665d4..e4157ea2d 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -37,34 +37,34 @@ #include "IconList.h" #include #include +#include #include #include #include -#include #include #include #include "icons/IconUtils.h" #define MAX_SIZE 1024 -IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent) +IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent) { QSet builtinNames; // add builtin icons - for (const auto& builtinPath : builtinPaths) { - QDir instanceIcons(builtinPath); - auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); - for (const auto& fileInfo : fileInfoList) { - builtinNames.insert(fileInfo.completeBaseName()); + for (auto& builtinPath : builtinPaths) { + QDir instance_icons(builtinPath); + auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); + for (auto file_info : file_info_list) { + builtinNames.insert(file_info.completeBaseName()); } } - for (const auto& builtinName : builtinNames) { + for (auto& builtinName : builtinNames) { addThemeIcon(builtinName); } m_watcher.reset(new QFileSystemWatcher()); - m_isWatching = false; + is_watching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); @@ -77,129 +77,91 @@ IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject void IconList::sortIconList() { qDebug() << "Sorting icon list..."; - std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) { - bool aIsSubdir = a.m_key.contains(QDir::separator()); - bool bIsSubdir = b.m_key.contains(QDir::separator()); - if (aIsSubdir != bIsSubdir) { - return !aIsSubdir; // root-level icons come first - } - return a.m_key.localeAwareCompare(b.m_key) < 0; - }); + std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; }); reindex(); } -// Helper function to add directories recursively -bool IconList::addPathRecursively(const QString& path) -{ - QDir dir(path); - if (!dir.exists()) - return false; - - // Add the directory itself - bool watching = m_watcher->addPath(path); - - // Add all subdirectories - QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QFileInfo& entry : entries) { - if (addPathRecursively(entry.absoluteFilePath())) { - watching = true; - } - } - return watching; -} - -QStringList IconList::getIconFilePaths() const -{ - QStringList iconFiles{}; - QStringList directories{ m_dir.absolutePath() }; - while (!directories.isEmpty()) { - QString first = directories.takeFirst(); - QDir dir(first); - for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { - if (fileInfo.isDir()) - directories.push_back(fileInfo.absoluteFilePath()); - else - iconFiles.push_back(fileInfo.absoluteFilePath()); - } - } - return iconFiles; -} - -QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) -{ - if (iconFile.dir() == iconsDir) - return iconFile.completeBaseName(); - - constexpr auto delimiter = " » "; - QString relativePathWithoutExtension = - iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); - return relativePathWithoutExtension.replace(QDir::separator(), delimiter); -} - -/// Split into a separate function because the preprocessing impedes readability -QSet toStringSet(const QList& list) -{ - QSet set(list.begin(), list.end()); - return set; -} - void IconList::directoryChanged(const QString& path) { - QDir newDir(path); - if (m_dir.absolutePath() != newDir.absolutePath()) { + QDir new_dir(path); + if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); - if (m_isWatching) + if (is_watching) stopWatching(); startWatching(); } - if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) - return; + if (!m_dir.exists()) + if (!FS::ensureFolderPathExists(m_dir.absolutePath())) + return; m_dir.refresh(); - const QStringList newFileNamesList = getIconFilePaths(); - const QSet newSet = toStringSet(newFileNamesList); - QSet currentSet; - for (const MMCIcon& it : m_icons) { + auto new_list = m_dir.entryList(QDir::Files, QDir::Name); + for (auto it = new_list.begin(); it != new_list.end(); it++) { + QString& foo = (*it); + foo = m_dir.filePath(foo); + } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet new_set(new_list.begin(), new_list.end()); +#else + auto new_set = new_list.toSet(); +#endif + QList current_list; + for (auto& it : icons) { if (!it.has(IconType::FileBased)) continue; - QFileInfo icon(it.getFilePath()); - currentSet.insert(icon.absoluteFilePath()); + current_list.push_back(it.m_images[IconType::FileBased].filename); } - QSet toRemove = currentSet - newSet; - QSet toAdd = newSet - currentSet; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QSet current_set(current_list.begin(), current_list.end()); +#else + QSet current_set = current_list.toSet(); +#endif - for (const QString& removedPath : toRemove) { - qDebug() << "Removing icon" << removedPath; - QFileInfo removedFile(removedPath); - QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); - QString key = QFileInfo(relativePath).completeBaseName(); + QSet to_remove = current_set; + to_remove -= new_set; + + QSet to_add = new_set; + to_add -= current_set; + + for (auto remove : to_remove) { + qDebug() << "Removing " << remove; + QFileInfo rmfile(remove); + QString key = rmfile.completeBaseName(); + + QString suffix = rmfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (!IconUtils::isIconSuffix(suffix)) + key = rmfile.fileName(); int idx = getIconIndex(key); if (idx == -1) continue; - m_icons[idx].remove(FileBased); - if (m_icons[idx].type() == ToBeDeleted) { + icons[idx].remove(IconType::FileBased); + if (icons[idx].type() == IconType::ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); - m_icons.remove(idx); + icons.remove(idx); reindex(); endRemoveRows(); } else { dataChanged(index(idx), index(idx)); } - m_watcher->removePath(removedPath); + m_watcher->removePath(remove); emit iconUpdated(key); } - for (const QString& addedPath : toAdd) { - qDebug() << "Adding icon" << addedPath; + for (auto add : to_add) { + qDebug() << "Adding " << add; - QFileInfo addfile(addedPath); - QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); - QString key = QFileInfo(relativePath).completeBaseName(); - QString name = formatName(m_dir, addfile); + QFileInfo addfile(add); + QString key = addfile.completeBaseName(); - if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { - m_watcher->addPath(addedPath); + QString suffix = addfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (!IconUtils::isIconSuffix(suffix)) + key = addfile.fileName(); + + if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { + m_watcher->addPath(add); emit iconUpdated(key); } } @@ -209,30 +171,24 @@ void IconList::directoryChanged(const QString& path) void IconList::fileChanged(const QString& path) { - qDebug() << "Checking icon" << path; + qDebug() << "Checking " << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); + QString key = checkfile.completeBaseName(); int idx = getIconIndex(key); if (idx == -1) return; - 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()) + QIcon icon(path); + if (!icon.availableSizes().size()) return; - m_icons[idx].m_images[IconType::FileBased].icon = icon; + icons[idx].m_images[IconType::FileBased].icon = icon; dataChanged(index(idx), index(idx)); emit iconUpdated(key); } -void IconList::SettingChanged(const Setting& setting, const QVariant& value) +void IconList::SettingChanged(const Setting& setting, QVariant value) { if (setting.id() != "IconsDir") return; @@ -244,11 +200,11 @@ void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); - m_isWatching = addPathRecursively(abs_path); - if (m_isWatching) { - qDebug() << "Started watching" << abs_path; + is_watching = m_watcher->addPath(abs_path); + if (is_watching) { + qDebug() << "Started watching " << abs_path; } else { - qDebug() << "Failed to start watching" << abs_path; + qDebug() << "Failed to start watching " << abs_path; } } @@ -256,7 +212,7 @@ void IconList::stopWatching() { m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); - m_isWatching = false; + is_watching = false; } QStringList IconList::mimeTypes() const @@ -286,7 +242,7 @@ bool IconList::dropMimeData(const QMimeData* data, if (data->hasUrls()) { auto urls = data->urls(); QStringList iconFiles; - for (const auto& url : urls) { + for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; @@ -307,33 +263,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const QVariant IconList::data(const QModelIndex& index, int role) const { if (!index.isValid()) - return {}; + return QVariant(); int row = index.row(); - if (row < 0 || row >= m_icons.size()) - return {}; + if (row < 0 || row >= icons.size()) + return QVariant(); switch (role) { case Qt::DecorationRole: - return m_icons[row].icon(); + return icons[row].icon(); case Qt::DisplayRole: - return m_icons[row].name(); + return icons[row].name(); case Qt::UserRole: - return m_icons[row].m_key; + return icons[row].m_key; default: - return {}; + return QVariant(); } } int IconList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : m_icons.size(); + return parent.isValid() ? 0 : icons.size(); } void IconList::installIcons(const QStringList& iconFiles) { - for (const QString& file : iconFiles) + for (QString file : iconFiles) installIcon(file, {}); } @@ -356,13 +312,12 @@ bool IconList::iconFileExists(const QString& key) const return iconEntry && iconEntry->has(IconType::FileBased); } -/// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) return nullptr; - return &m_icons[iconIdx]; + return &icons[iconIdx]; } bool IconList::deleteIcon(const QString& key) @@ -377,22 +332,22 @@ bool IconList::trashIcon(const QString& key) bool IconList::addThemeIcon(const QString& key) { - auto iter = m_nameIndex.find(key); - if (iter != m_nameIndex.end()) { - auto& oldOne = m_icons[*iter]; + auto iter = name_index.find(key); + if (iter != name_index.end()) { + auto& oldOne = icons[*iter]; oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); + beginInsertRows(QModelIndex(), icons.size(), icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = key; mmc_icon.m_key = key; mmc_icon.replace(Builtin, key); - m_icons.push_back(mmc_icon); - m_nameIndex[key] = m_icons.size() - 1; + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; } endInsertRows(); return true; @@ -401,32 +356,25 @@ 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; - // 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); - } - + QIcon icon(path); if (icon.isNull()) return false; - auto iter = m_nameIndex.find(key); - if (iter != m_nameIndex.end()) { - auto& oldOne = m_icons[*iter]; + auto iter = name_index.find(key); + if (iter != name_index.end()) { + auto& oldOne = icons[*iter]; oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); + beginInsertRows(QModelIndex(), icons.size(), icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = name; mmc_icon.m_key = key; mmc_icon.replace(type, icon, path); - m_icons.push_back(mmc_icon); - m_nameIndex[key] = m_icons.size() - 1; + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; } endInsertRows(); return true; @@ -441,32 +389,33 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for void IconList::reindex() { - m_nameIndex.clear(); - for (int i = 0; i < m_icons.size(); i++) { - m_nameIndex[m_icons[i].m_key] = i; - emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model + name_index.clear(); + int i = 0; + for (auto& iter : icons) { + name_index[iter.m_key] = i; + i++; } } QIcon IconList::getIcon(const QString& key) const { - int iconIndex = getIconIndex(key); + int icon_index = getIconIndex(key); - if (iconIndex != -1) - return m_icons[iconIndex].icon(); + if (icon_index != -1) + return icons[icon_index].icon(); - // Fallback for icons that don't exist.b - iconIndex = getIconIndex("grass"); + // Fallback for icons that don't exist. + icon_index = getIconIndex("grass"); - if (iconIndex != -1) - return m_icons[iconIndex].icon(); - return {}; + if (icon_index != -1) + return icons[icon_index].icon(); + return QIcon(); } int IconList::getIconIndex(const QString& key) const { - auto iter = m_nameIndex.find(key == "default" ? "grass" : key); - if (iter != m_nameIndex.end()) + auto iter = name_index.find(key == "default" ? "grass" : key); + if (iter != name_index.end()) return *iter; return -1; @@ -476,15 +425,3 @@ QString IconList::getDirectory() const { return m_dir.absolutePath(); } - -/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. -QString IconList::iconDirectory(const QString& key) const -{ - for (const auto& mmcIcon : m_icons) { - if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) { - QFileInfo iconFile(mmcIcon.getFilePath()); - return iconFile.dir().path(); - } - } - return getDirectory(); -} diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index d2f904448..553946c42 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -51,7 +51,7 @@ class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: - explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); + explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); virtual ~IconList() {}; QIcon getIcon(const QString& key) const; @@ -72,7 +72,6 @@ class IconList : public QAbstractListModel { bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; - QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); @@ -92,20 +91,18 @@ class IconList : public QAbstractListModel { IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); - bool addPathRecursively(const QString& path); - QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); protected slots: void fileChanged(const QString& path); - void SettingChanged(const Setting& setting, const QVariant& value); + void SettingChanged(const Setting& setting, QVariant value); private: shared_qobject_ptr m_watcher; - bool m_isWatching; - QMap m_nameIndex; - QList m_icons; + bool is_watching; + QMap name_index; + QVector icons; QDir m_dir; }; diff --git a/launcher/include/base.pch.hpp b/launcher/include/base.pch.hpp deleted file mode 100644 index ecaf41fdd..000000000 --- a/launcher/include/base.pch.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#ifndef PRISM_PRECOMPILED_BASE_HEADERS_H -#define PRISM_PRECOMPILED_BASE_HEADERS_H - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#endif // PRISM_PRECOMPILED_BASE_HEADERS_H diff --git a/launcher/include/qtcore.pch.hpp b/launcher/include/qtcore.pch.hpp deleted file mode 100644 index b8836618a..000000000 --- a/launcher/include/qtcore.pch.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once -#ifndef PRISM_PRECOMPILED_QTCORE_HEADERS_H -#define PRISM_PRECOMPILED_QTCORE_HEADERS_H - -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include - -#include - -// collections -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include - -#include - -#include -#include -#include - -#endif // PRISM_PRECOMPILED_QTCORE_HEADERS_H diff --git a/launcher/include/qtgui.pch.hpp b/launcher/include/qtgui.pch.hpp deleted file mode 100644 index fb57cb33a..000000000 --- a/launcher/include/qtgui.pch.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#ifndef PRISM_PRECOMPILED_QTGUI_HEADERS_H -#define PRISM_PRECOMPILED_QTGUI_HEADERS_H - -#include - -#include - -#include - -#include - -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include - -#include -#include -#include - -#include -#include - -#include -#include -#include -#include - -#endif // PRISM_PRECOMPILED_GUI_HEADERS_H diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in new file mode 100644 index 000000000..acbce9650 --- /dev/null +++ b/launcher/install_prereqs.cmake.in @@ -0,0 +1,26 @@ +set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") +file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") +function(gp_resolved_file_type_override resolved_file type_var) + if(resolved_file MATCHES "^/(usr/)?lib/libQt") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libxcb-") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libicu") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libpng") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libproxy") + set(${type_var} other PARENT_SCOPE) + elseif((resolved_file MATCHES "^/(usr/)?lib(.+)?/libstdc\\+\\+") AND (UNIX AND NOT APPLE)) + set(${type_var} other PARENT_SCOPE) + endif() +endfunction() + +set(gp_tool "@CMAKE_GP_TOOL@") +set(gp_cmd_paths ${gp_cmd_paths} + "@CMAKE_GP_CMD_PATHS@" +) + +include(BundleUtilities) +fixup_bundle("@APPS@" "${QTPLUGINS}" "@DIRS@") + diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 3a04756fa..772c90e42 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -84,7 +84,7 @@ void JavaChecker::executeTask() process->setProcessEnvironment(CleanEnviroment()); qDebug() << "Running java checker:" << m_path << args.join(" "); - connect(process.get(), &QProcess::finished, this, &JavaChecker::finished); + connect(process.get(), QOverload::of(&QProcess::finished), this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady); connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady); @@ -137,7 +137,11 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QMap results; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); +#else + QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); +#endif for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux @@ -145,7 +149,11 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) continue; } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto parts = line.split('=', Qt::SkipEmptyParts); +#else + auto parts = line.split('=', QString::SkipEmptyParts); +#endif if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { @@ -163,7 +171,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" || os_arch == "ppc64le" || os_arch == "ppc64"; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64"; result.validity = Result::Validity::Valid; result.is_64bit = is_64; @@ -179,20 +187,13 @@ 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:" << process->errorString(); + qDebug() << "Java checker has failed to start."; qDebug() << "Process environment:"; qDebug() << process->environment(); qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); - - Result result = { - m_path, - m_id, - }; - result.errorLog = process->errorString(); - result.validity = Result::Validity::Errored; - emit checkFinished(result); + emit checkFinished({ m_path, m_id }); } emitSucceeded(); } diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 98aac5cab..8e97e0e14 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -21,7 +21,7 @@ #include "BaseVersion.h" #include "StringUtils.h" -bool JavaInstall::operator<(const JavaInstall& rhs) const +bool JavaInstall::operator<(const JavaInstall& rhs) { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if (archCompare != 0) @@ -35,30 +35,30 @@ bool JavaInstall::operator<(const JavaInstall& rhs) const return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } -bool JavaInstall::operator==(const JavaInstall& rhs) const +bool JavaInstall::operator==(const JavaInstall& rhs) { return arch == rhs.arch && id == rhs.id && path == rhs.path; } -bool JavaInstall::operator>(const JavaInstall& rhs) const +bool JavaInstall::operator>(const JavaInstall& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } -bool JavaInstall::operator<(BaseVersion& a) const +bool JavaInstall::operator<(BaseVersion& a) { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast&) { + } catch (const std::bad_cast& e) { return BaseVersion::operator<(a); } } -bool JavaInstall::operator>(BaseVersion& a) const +bool JavaInstall::operator>(BaseVersion& a) { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast&) { + } catch (const std::bad_cast& e) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 5899964f0..7d8d392fa 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -24,21 +24,22 @@ struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} - virtual QString descriptor() const override { return id.toString(); } + virtual QString descriptor() override { return id.toString(); } - virtual QString name() const override { return id.toString(); } + virtual QString name() override { return id.toString(); } virtual QString typeString() const override { return arch; } - virtual bool operator<(BaseVersion& a) const override; - virtual bool operator>(BaseVersion& a) const override; - bool operator<(const JavaInstall& rhs) const; - bool operator==(const JavaInstall& rhs) const; - bool operator>(const JavaInstall& rhs) const; + virtual bool operator<(BaseVersion& a) override; + virtual bool operator>(BaseVersion& a) override; + bool operator<(const JavaInstall& rhs); + bool operator==(const JavaInstall& rhs); + bool operator>(const JavaInstall& rhs); 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 1d7b9cdff..aa7fab8a0 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -41,7 +41,6 @@ #include #include "Application.h" -#include "settings/SettingsObject.h" #include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -51,9 +50,8 @@ JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) {} -Task::Ptr JavaInstallList::getLoadTask(bool forceReload) +Task::Ptr JavaInstallList::getLoadTask() { - Q_UNUSED(forceReload) load(); return getCurrentTask(); } @@ -109,7 +107,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const case VersionRole: return version->id.toString(); case RecommendedRole: - return false; + return version->recommended; case PathRole: return version->path; case CPUArchitectureRole: @@ -129,6 +127,10 @@ 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 58d1ad8ca..b77f17b28 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(bool forceReload = false) override; + [[nodiscard]] Task::Ptr getLoadTask() 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 3647c963f..2d68f55c8 100644 --- a/launcher/java/JavaMetadata.cpp +++ b/launcher/java/JavaMetadata.cpp @@ -52,33 +52,33 @@ MetadataPtr parseJavaMeta(const QJsonObject& in) { auto meta = std::make_shared(); - meta->m_name = in["name"].toString(""); - meta->vendor = in["vendor"].toString(""); - meta->url = in["url"].toString(""); - meta->releaseTime = timeFromS3Time(in["releaseTime"].toString("")); - meta->downloadType = parseDownloadType(in["downloadType"].toString("")); - meta->packageType = in["packageType"].toString(""); - meta->runtimeOS = in["runtimeOS"].toString("unknown"); + meta->m_name = Json::ensureString(in, "name", ""); + meta->vendor = Json::ensureString(in, "vendor", ""); + meta->url = Json::ensureString(in, "url", ""); + meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); + meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", "")); + meta->packageType = Json::ensureString(in, "packageType", ""); + meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); if (in.contains("checksum")) { auto obj = Json::requireObject(in, "checksum"); - meta->checksumHash = obj["hash"].toString(""); - meta->checksumType = obj["type"].toString(""); + meta->checksumHash = Json::ensureString(obj, "hash", ""); + meta->checksumType = Json::ensureString(obj, "type", ""); } if (in.contains("version")) { auto obj = Json::requireObject(in, "version"); - auto name = obj["name"].toString(""); - auto major = obj["major"].toInteger(); - auto minor = obj["minor"].toInteger(); - auto security = obj["security"].toInteger(); - auto build = obj["build"].toInteger(); + auto name = Json::ensureString(obj, "name", ""); + auto major = Json::ensureInteger(obj, "major", 0); + auto minor = Json::ensureInteger(obj, "minor", 0); + auto security = Json::ensureInteger(obj, "security", 0); + auto build = Json::ensureInteger(obj, "build", 0); meta->version = JavaVersion(major, minor, security, build, name); } return meta; } -bool Metadata::operator<(const Metadata& rhs) const +bool Metadata::operator<(const Metadata& rhs) { auto id = version; if (id < rhs.version) { @@ -97,30 +97,30 @@ bool Metadata::operator<(const Metadata& rhs) const return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; } -bool Metadata::operator==(const Metadata& rhs) const +bool Metadata::operator==(const Metadata& rhs) { return version == rhs.version && m_name == rhs.m_name; } -bool Metadata::operator>(const Metadata& rhs) const +bool Metadata::operator>(const Metadata& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } -bool Metadata::operator<(BaseVersion& a) const +bool Metadata::operator<(BaseVersion& a) { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast&) { + } catch (const std::bad_cast& e) { return BaseVersion::operator<(a); } } -bool Metadata::operator>(BaseVersion& a) const +bool Metadata::operator>(BaseVersion& a) { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast&) { + } catch (const std::bad_cast& e) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h index 0757a6935..77a42fd78 100644 --- a/launcher/java/JavaMetadata.h +++ b/launcher/java/JavaMetadata.h @@ -32,17 +32,17 @@ enum class DownloadType { Manifest, Archive, Unknown }; class Metadata : public BaseVersion { public: - virtual QString descriptor() const override { return version.toString(); } + virtual QString descriptor() override { return version.toString(); } - virtual QString name() const override { return m_name; } + virtual QString name() override { return m_name; } virtual QString typeString() const override { return vendor; } - virtual bool operator<(BaseVersion& a) const override; - virtual bool operator>(BaseVersion& a) const override; - bool operator<(const Metadata& rhs) const; - bool operator==(const Metadata& rhs) const; - bool operator>(const Metadata& rhs) const; + virtual bool operator<(BaseVersion& a) override; + virtual bool operator>(BaseVersion& a) override; + bool operator<(const Metadata& rhs); + bool operator==(const Metadata& rhs); + bool operator>(const Metadata& rhs); QString m_name; QString vendor; @@ -61,4 +61,4 @@ DownloadType parseDownloadType(QString javaDownload); QString downloadTypeToString(DownloadType javaDownload); MetadataPtr parseJavaMeta(const QJsonObject& libObj); -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index c58fe5601..f3200428e 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -42,7 +42,6 @@ #include #include "Application.h" -#include "BuildConfig.h" #include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -156,7 +155,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() QStringList addJavasFromEnv(QList javas) { - auto env = QProcessEnvironment::systemEnvironment().value(QStringLiteral("%1_JAVA_PATHS").arg(BuildConfig.LAUNCHER_ENVNAME)); + auto env = qEnvironmentVariable("PRISMLAUNCHER_JAVA_PATHS"); // FIXME: use launcher name from buildconfig #if defined(Q_OS_WIN32) QList javaPaths = env.replace("\\", "/").split(QLatin1String(";")); @@ -366,13 +365,13 @@ QList JavaUtils::FindJavaPaths() javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& java : libraryJVMJavas) { + foreach (const QString& java, libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& java : systemLibraryJVMJavas) { + foreach (const QString& java, systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -380,25 +379,16 @@ QList JavaUtils::FindJavaPaths() auto home = qEnvironmentVariable("HOME"); // javas downloaded by sdkman - QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); - QDir sdkmanJavaDir(FS::PathCombine(sdkmanDir, "candidates/java")); - QStringList sdkmanJavas = sdkmanJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& java : sdkmanJavas) { - javas.append(sdkmanJavaDir.absolutePath() + "/" + java + "/bin/java"); - } - - // javas downloaded by asdf - QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); - QDir asdfJavaDir(FS::PathCombine(asdfDataDir, "installs/java")); - QStringList asdfJavas = asdfJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& java : asdfJavas) { - javas.append(asdfJavaDir.absolutePath() + "/" + java + "/bin/java"); + QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); + QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString& java, sdkmanJavas) { + javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& java : userLibraryJVMJavas) { + foreach (const QString& java, userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -415,7 +405,7 @@ QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); - auto scanJavaDir = [&javas]( + auto scanJavaDir = [&]( const QString& dirPath, const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); @@ -434,7 +424,7 @@ QList JavaUtils::FindJavaPaths() }; // java installed in a snap is installed in the standard directory, but underneath $SNAP auto snap = qEnvironmentVariable("SNAP"); - auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) { + auto scanJavaDirs = [&](const QString& dirPath) { scanJavaDir(dirPath); if (!snap.isNull()) { scanJavaDir(snap + dirPath); @@ -452,15 +442,9 @@ QList JavaUtils::FindJavaPaths() QString fileName = info.fileName(); return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); }; - // AOSC OS's locations for openjdk - auto aoscFilter = [](const QFileInfo& info) { - QString fileName = info.fileName(); - return fileName == "java" || fileName.startsWith("java-"); - }; scanJavaDir("/usr/lib64", gentooFilter); scanJavaDir("/usr/lib", gentooFilter); scanJavaDir("/opt", gentooFilter); - scanJavaDir("/usr/lib", aoscFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt @@ -478,14 +462,9 @@ QList JavaUtils::FindJavaPaths() // javas downloaded by IntelliJ scanJavaDirs(FS::PathCombine(home, ".jdks")); // javas downloaded by sdkman - QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); - scanJavaDirs(FS::PathCombine(sdkmanDir, "candidates/java")); - // javas downloaded by asdf - QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); - scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); + scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); // javas downloaded by gradle (toolchains) - QString gradleUserHome = qEnvironmentVariable("GRADLE_USER_HOME", FS::PathCombine(home, ".gradle")); - scanJavaDirs(FS::PathCombine(gradleUserHome, "jdks")); + scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); @@ -516,7 +495,7 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { QStringList processpaths; -#if defined(Q_OS_MACOS) +#if defined(Q_OS_OSX) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) @@ -567,12 +546,12 @@ QStringList getPrismJavaBundle() { QList javas; - auto scanDir = [&javas](QString prefix) { + auto scanDir = [&](QString prefix) { javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); }; - auto scanJavaDir = [scanDir](const QString& dirPath) { + auto scanJavaDir = [&](const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index fef573c16..bca50f2c9 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -19,13 +19,9 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString) QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { - static const QRegularExpression s_withOne( - "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); - pattern = s_withOne; + pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); } else { - static const QRegularExpression s_withoutOne( - "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); - pattern = s_withoutOne; + pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); } auto match = pattern.match(m_string); @@ -63,7 +59,7 @@ bool JavaVersion::isModular() const return m_parseable && m_major >= 9; } -bool JavaVersion::operator<(const JavaVersion& rhs) const +bool JavaVersion::operator<(const JavaVersion& rhs) { if (m_parseable && rhs.m_parseable) { auto major = m_major; @@ -101,7 +97,7 @@ bool JavaVersion::operator<(const JavaVersion& rhs) const return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } -bool JavaVersion::operator==(const JavaVersion& rhs) const +bool JavaVersion::operator==(const JavaVersion& rhs) { if (m_parseable && rhs.m_parseable) { return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; @@ -109,7 +105,7 @@ bool JavaVersion::operator==(const JavaVersion& rhs) const return m_string == rhs.m_string; } -bool JavaVersion::operator>(const JavaVersion& rhs) const +bool JavaVersion::operator>(const JavaVersion& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index 143ddd262..c070bdeec 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -20,9 +20,9 @@ class JavaVersion { JavaVersion& operator=(const QString& rhs); - bool operator<(const JavaVersion& rhs) const; - bool operator==(const JavaVersion& rhs) const; - bool operator>(const JavaVersion& rhs) const; + bool operator<(const JavaVersion& rhs); + bool operator==(const JavaVersion& rhs); + bool operator>(const JavaVersion& rhs); bool requiresPermGen() const; bool defaultsToUtf8() const; diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index c60908cec..bb7cc568d 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -16,11 +16,12 @@ * along with this program. If not, see . */ #include "java/download/ArchiveDownloadTask.h" +#include #include +#include "MMCZip.h" #include "Application.h" -#include "archive/ArchiveReader.h" -#include "archive/ExtractZipTask.h" +#include "Untar.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "tasks/Task.h" @@ -54,7 +55,6 @@ void ArchiveDownloadTask::executeTask() connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); - connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); @@ -66,45 +66,68 @@ void ArchiveDownloadTask::executeTask() void ArchiveDownloadTask::extractJava(QString input) { setStatus(tr("Extracting Java")); + if (input.endsWith("tar")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + QFile in(input); + if (!in.open(QFile::ReadOnly)) { + emitFailed(tr("Unable to open supplied tar file.")); + return; + } + if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("zip")) { + auto zip = std::make_shared(input); + if (!zip->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + auto files = zip->getFileNameList(); + if (files.isEmpty()) { + emitFailed(tr("No files were found in the supplied zip file.")); + return; + } + m_task = makeShared(zip, m_final_path, files[0]); - MMCZip::ArchiveReader zip(input); - if (!zip.collectFiles()) { - emitFailed(tr("Unable to open supplied zip file.")); + auto progressStep = std::make_shared(); + connect(m_task.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); + connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + + connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); return; } - auto files = zip.getFiles(); - if (files.isEmpty()) { - emitFailed(tr("No files were found in the supplied zip file.")); - return; - } - auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts); - m_task = makeShared(input, m_final_path, firstFolderParts.value(0)); - auto progressStep = std::make_shared(); - connect(m_task.get(), &Task::finished, this, [this, progressStep] { - progressStep->state = TaskStepState::Succeeded; - stepProgress(*progressStep); - }); - - connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); - connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); - connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { - progressStep->state = TaskStepState::Failed; - stepProgress(*progressStep); - emitFailed(reason); - }); - connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); - - connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { - progressStep->update(current, total); - stepProgress(*progressStep); - }); - connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { - progressStep->status = status; - stepProgress(*progressStep); - }); - m_task->start(); - return; + emitFailed(tr("Could not determine archive type!")); } bool ArchiveDownloadTask::abort() @@ -112,6 +135,7 @@ bool ArchiveDownloadTask::abort() auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); + emitAborted(); return aborted; }; -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h index cfcdf9dcf..1db33763a 100644 --- a/launcher/java/download/ArchiveDownloadTask.h +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -28,7 +28,7 @@ class ArchiveDownloadTask : public Task { ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ArchiveDownloadTask() = default; - bool canAbort() const override { return true; } + [[nodiscard]] bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; @@ -42,4 +42,4 @@ class ArchiveDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 0a51741a2..20b39e751 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -39,8 +39,9 @@ void ManifestDownloadTask::executeTask() { setStatus(tr("Downloading Java")); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto files = std::make_shared(); - auto [action, files] = Net::Download::makeByteArray(m_url); + auto action = Net::Download::makeByteArray(m_url, files); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { @@ -60,7 +61,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; @@ -76,27 +77,27 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) // valid json doc, begin making jre spot FS::ensureFolderPathExists(m_final_path); std::vector toDownload; - auto list = doc.object()["files"].toObject(); + auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); for (const auto& paths : list.keys()) { auto file = FS::PathCombine(m_final_path, paths); - const QJsonObject& meta = list[paths].toObject(); - auto type = meta["type"].toString(); + const QJsonObject& meta = Json::ensureObject(list, paths); + auto type = Json::ensureString(meta, "type"); if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { // this is *nix only ! - auto path = meta["target"].toString(); + auto path = Json::ensureString(meta, "target"); if (!path.isEmpty()) { QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? - auto raw = meta["downloads"].toObject()["raw"].toObject(); - auto isExec = meta["executable"].toBool(); - auto url = raw["url"].toString(); + auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw"); + auto isExec = Json::ensureBoolean(meta, "executable", false); + auto url = Json::ensureString(raw, "url"); if (!url.isEmpty() && QUrl(url).isValid()) { - auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec }; + auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec }; toDownload.push_back(f); } } @@ -133,4 +134,4 @@ bool ManifestDownloadTask::abort() emitAborted(); return aborted; }; -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h index e68c8236f..ae9e0d0ed 100644 --- a/launcher/java/download/ManifestDownloadTask.h +++ b/launcher/java/download/ManifestDownloadTask.h @@ -29,7 +29,7 @@ class ManifestDownloadTask : public Task { ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ManifestDownloadTask() = default; - bool canAbort() const override { return true; } + [[nodiscard]] bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; @@ -43,4 +43,4 @@ class ManifestDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/SymlinkTask.cpp b/launcher/java/download/SymlinkTask.cpp index 9bbd50c63..843c7caa9 100644 --- a/launcher/java/download/SymlinkTask.cpp +++ b/launcher/java/download/SymlinkTask.cpp @@ -78,4 +78,4 @@ void SymlinkTask::executeTask() } } -} // namespace Java +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/SymlinkTask.h b/launcher/java/download/SymlinkTask.h index e38323eae..88cb20dd7 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 +} // namespace Java \ No newline at end of file diff --git a/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h index 80dcd31e9..d49d7545b 100644 --- a/launcher/launch/LaunchStep.h +++ b/launcher/launch/LaunchStep.h @@ -28,8 +28,8 @@ class LaunchStep : public Task { virtual ~LaunchStep() = default; signals: - void logLines(QStringList lines, MessageLevel level); - void logLine(QString line, MessageLevel level); + void logLines(QStringList lines, MessageLevel::Enum level); + void logLine(QString line, MessageLevel::Enum level); void readyForLaunch(); void progressReportingRequest(); diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 26b2b582d..0251b302d 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,12 +37,12 @@ #include "launch/LaunchTask.h" #include -#include #include #include #include +#include +#include #include -#include #include "MessageLevel.h" #include "tasks/Task.h" @@ -51,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -std::unique_ptr LaunchTask::create(MinecraftInstance* inst) +shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) { - auto task = std::unique_ptr(new LaunchTask(inst)); - task->init(); - return task; + shared_qobject_ptr proc(new LaunchTask(inst)); + proc->init(); + return proc; } -LaunchTask::LaunchTask(MinecraftInstance* instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { @@ -76,7 +76,6 @@ void LaunchTask::executeTask() if (!m_steps.size()) { state = LaunchTask::Finished; emitSucceeded(); - return; } state = LaunchTask::Running; onStepFinished(); @@ -180,7 +179,7 @@ bool LaunchTask::abort() return true; case LaunchTask::NotStarted: { state = LaunchTask::Aborted; - emitAborted(); + emitFailed("Aborted"); return true; } case LaunchTask::Running: @@ -204,8 +203,8 @@ shared_qobject_ptr LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); - m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); - m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); + m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); + m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" @@ -215,77 +214,31 @@ shared_qobject_ptr LaunchTask::getLogModel() return m_logModel; } -bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level) -{ - LogParser* parser; - switch (static_cast(level)) { - case MessageLevel::StdErr: - parser = &m_stderrParser; - break; - case MessageLevel::StdOut: - parser = &m_stdoutParser; - break; - default: - return false; - } - - parser->appendLine(line); - auto items = parser->parseAvailable(); - if (auto err = parser->getError(); err.has_value()) { - auto& model = *getLogModel(); - model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); - return false; - } - - if (items.isEmpty()) - return true; - - auto model = getLogModel(); - for (auto const& item : items) { - if (std::holds_alternative(item)) { - auto entry = std::get(item); - auto msg = QString("[%1] [%2/%3] [%4]: %5") - .arg(entry.timestamp.toString("HH:mm:ss")) - .arg(entry.thread) - .arg(entry.levelText) - .arg(entry.logger) - .arg(entry.message); - msg = censorPrivateInfo(msg); - model->append(entry.level, msg); - } else if (std::holds_alternative(item)) { - auto msg = std::get(item).message; - - MessageLevel newLevel = MessageLevel::takeFromLine(msg); - - if (newLevel == MessageLevel::Unknown) - newLevel = LogParser::guessLevel(line, model->previousLevel()); - - msg = censorPrivateInfo(msg); - - model->append(newLevel, msg); - } - } - - return true; -} - -void LaunchTask::onLogLines(const QStringList& lines, MessageLevel defaultLevel) +void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel) { for (auto& line : lines) { onLogLine(line, defaultLevel); } } -void LaunchTask::onLogLine(QString line, MessageLevel level) +void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) { - if (parseXmlLogs(line, level)) { - return; + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if (innerLevel != MessageLevel::Unknown) { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + level = m_instance->guessLevel(line, level); } // censor private user info line = censorPrivateInfo(line); - getLogModel()->append(level, line); + auto& model = *getLogModel(); + model.append(level, line); } void LaunchTask::emitSucceeded() @@ -301,60 +254,20 @@ void LaunchTask::emitFailed(QString reason) Task::emitFailed(reason); } -QString expandVariables(const QString& input, QProcessEnvironment dict) +void LaunchTask::substituteVariables(QStringList& args) const { - QString result = input; + auto env = m_instance->createEnvironment(); - enum { base, maybeBrace, variable, brace } state = base; - int startIdx = -1; - for (int i = 0; i < result.length();) { - QChar c = result.at(i++); - switch (state) { - case base: - if (c == '$') - state = maybeBrace; - break; - case maybeBrace: - if (c == '{') { - state = brace; - startIdx = i; - } else if (c.isLetterOrNumber() || c == '_') { - state = variable; - startIdx = i - 1; - } else { - state = base; - } - break; - case brace: - if (c == '}') { - const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), ""); - if (!res.isEmpty()) { - result.replace(startIdx - 2, i - startIdx + 2, res); - i = startIdx - 2 + res.length(); - } - state = base; - } - break; - case variable: - if (!c.isLetterOrNumber() && c != '_') { - const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), ""); - if (!res.isEmpty()) { - result.replace(startIdx - 1, i - startIdx, res); - i = startIdx - 1 + res.length(); - } - state = base; - } - break; - } + for (auto key : env.keys()) { + args.replaceInStrings("$" + key, env.value(key)); } - if (state == variable) { - if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty()) - result.replace(startIdx - 1, result.length() - startIdx + 1, res); - } - return result; } -QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const +void LaunchTask::substituteVariables(QString& cmd) const { - return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment()); + auto env = m_instance->createEnvironment(); + + for (auto key : env.keys()) { + cmd.replace("$" + key, env.value(key)); + } } diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index c52273a9e..56065af5b 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -39,29 +39,29 @@ #include #include #include +#include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" -#include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(MinecraftInstance* instance); + explicit LaunchTask(MinecraftInstancePtr instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static std::unique_ptr create(MinecraftInstance* inst); + static shared_qobject_ptr create(MinecraftInstancePtr inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - MinecraftInstance* instance() { return m_instance; } + MinecraftInstancePtr instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -87,7 +87,8 @@ class LaunchTask : public Task { shared_qobject_ptr getLogModel(); public: - QString substituteVariables(QString& cmd, bool isLaunch = false) const; + void substituteVariables(QStringList& args) const; + void substituteVariables(QString& cmd) const; QString censorPrivateInfo(QString in); protected: /* methods */ @@ -105,8 +106,8 @@ class LaunchTask : public Task { void requestLogging(); public slots: - void onLogLines(const QStringList& lines, MessageLevel defaultLevel = MessageLevel::Launcher); - void onLogLine(QString line, MessageLevel defaultLevel = MessageLevel::Launcher); + void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); void onReadyForLaunch(); void onStepFinished(); void onProgressReportingRequested(); @@ -114,17 +115,12 @@ class LaunchTask : public Task { private: /*methods */ void finalizeSteps(bool successful, const QString& error); - protected: - bool parseXmlLogs(QString const& line, MessageLevel level); - protected: /* data */ - MinecraftInstance* m_instance; + MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; - LogParser m_stdoutParser; - LogParser m_stderrParser; }; diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 117867c1d..23a33ae18 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -24,13 +24,13 @@ QVariant LogModel::data(const QModelIndex& index, int role) const return m_content[realRow].line; } if (role == LevelRole) { - return static_cast(m_content[realRow].level); + return m_content[realRow].level; } return QVariant(); } -void LogModel::append(MessageLevel level, QString line) +void LogModel::append(MessageLevel::Enum level, QString line) { if (m_suspended) { return; @@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines) return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary - QList newContent; + QVector newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over @@ -149,28 +149,3 @@ bool LogModel::wrapLines() const { return m_lineWrap; } - -void LogModel::setColorLines(bool state) -{ - if (m_colorLines != state) { - m_colorLines = state; - } -} - -bool LogModel::colorLines() const -{ - return m_colorLines; -} - -bool LogModel::isOverFlow() -{ - return m_numLines >= m_maxLines && m_stopOnOverflow; -} - -MessageLevel LogModel::previousLevel() -{ - if (m_numLines > 0) { - return m_content[m_numLines - 1].level; - } - return MessageLevel::Unknown; -} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 847a41f5f..18e51d7e3 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -12,7 +12,7 @@ class LogModel : public QAbstractListModel { int rowCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; - void append(MessageLevel, QString line); + void append(MessageLevel::Enum, QString line); void clear(); void suspend(bool suspend); @@ -24,25 +24,20 @@ class LogModel : public QAbstractListModel { void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); - bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; - void setColorLines(bool state); - bool colorLines() const; - - MessageLevel previousLevel(); enum Roles { LevelRole = Qt::UserRole }; private /* types */: struct entry { - MessageLevel level = MessageLevel::Unknown; + MessageLevel::Enum level; QString line; }; private: /* data */ - QList m_content; + QVector m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; @@ -52,7 +47,6 @@ class LogModel : public QAbstractListModel { QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; - bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) diff --git a/launcher/launch/TaskStepWrapper.cpp b/launcher/launch/TaskStepWrapper.cpp index acf790a8f..db9e8fad2 100644 --- a/launcher/launch/TaskStepWrapper.cpp +++ b/launcher/launch/TaskStepWrapper.cpp @@ -23,7 +23,10 @@ void TaskStepWrapper::executeTask() return; } connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); - propagateFromOther(m_task.get()); + connect(m_task.get(), &Task::progress, this, &TaskStepWrapper::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &TaskStepWrapper::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &TaskStepWrapper::setStatus); + connect(m_task.get(), &Task::details, this, &TaskStepWrapper::setDetails); emit progressReportingRequest(); } @@ -56,7 +59,9 @@ bool TaskStepWrapper::canAbort() const bool TaskStepWrapper::abort() { if (m_task && m_task->canAbort()) { - return m_task->abort(); + auto status = m_task->abort(); + emitFailed("Aborted."); + return status; } return Task::abort(); } diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 1afd5cd1d..0f8d27e94 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -36,6 +36,7 @@ #include "CheckJava.h" #include #include +#include #include #include #include @@ -67,7 +68,7 @@ void CheckJava::executeTask() emitFailed(QString("Java path is not valid.")); return; } else { - emit logLine("Java path is:\n " + m_javaPath, MessageLevel::Launcher); + emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); } if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -146,6 +147,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").arg(version, architecture, realArchitecture, vendor), + QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n").arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp index cb2f5d7de..4b67b3092 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(); - emitAborted(); + emitFailed("Aborted"); return true; } @@ -87,6 +87,6 @@ void LookupServerAddress::resolve(const QString& address, quint16 port) m_output->address = address; m_output->port = port; - m_dnsLookup->deleteLater(); emitSucceeded(); + m_dnsLookup->deleteLater(); } diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 6b960974e..725101224 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -47,17 +47,25 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PostLaunchCommand::executeTask() { - auto cmd = m_parent->substituteVariables(m_command); - emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); - auto args = QProcess::splitCommand(cmd); + // FIXME: where to put this? +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto args = QProcess::splitCommand(m_command); + m_parent->substituteVariables(args); + emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); const QString program = args.takeFirst(); m_process.start(program, args); +#else + m_parent->substituteVariables(m_command); + + emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher); + m_process.start(m_command); +#endif } void PostLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [&]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 7e843ca3f..6d071a66e 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -47,16 +47,25 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent) void PreLaunchCommand::executeTask() { - auto cmd = m_parent->substituteVariables(m_command); - emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); - auto args = QProcess::splitCommand(cmd); + // FIXME: where to put this? +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + auto args = QProcess::splitCommand(m_command); + m_parent->substituteVariables(args); + + emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher); const QString program = args.takeFirst(); m_process.start(program, args); +#else + m_parent->substituteVariables(m_command); + + emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher); + m_process.start(m_command); +#endif } void PreLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [&]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: diff --git a/launcher/launch/steps/PrintServers.cpp b/launcher/launch/steps/PrintServers.cpp index ac0e4bf83..ba96d37b9 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"; + addresses += "]\n\n"; m_server_to_address.insert(server, addresses); diff --git a/launcher/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp index f96d11343..0dec35b79 100644 --- a/launcher/launch/steps/TextPrint.cpp +++ b/launcher/launch/steps/TextPrint.cpp @@ -1,11 +1,11 @@ #include "TextPrint.h" -TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level) : LaunchStep(parent) +TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level) : LaunchStep(parent) { m_lines = lines; m_level = level; } -TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel level) : LaunchStep(parent) +TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level) : LaunchStep(parent) { m_lines.append(line); m_level = level; @@ -24,6 +24,6 @@ bool TextPrint::canAbort() const bool TextPrint::abort() { - emitAborted(); + emitFailed("Aborted."); return true; } diff --git a/launcher/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h index 4479a260a..a96c2f887 100644 --- a/launcher/launch/steps/TextPrint.h +++ b/launcher/launch/steps/TextPrint.h @@ -26,8 +26,8 @@ class TextPrint : public LaunchStep { Q_OBJECT public: - explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level); - explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel level); + explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level); + explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level); virtual ~TextPrint() {}; virtual void executeTask(); @@ -36,5 +36,5 @@ class TextPrint : public LaunchStep { private: QStringList m_lines; - MessageLevel m_level; + MessageLevel::Enum m_level; }; diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp deleted file mode 100644 index b808b35a3..000000000 --- a/launcher/logs/AnonymizeLog.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2025 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "AnonymizeLog.h" - -#include - -struct RegReplace { - RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } - QRegularExpression reg; - QString with; -}; - -static const QVector anonymizeRules = { - RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), - "C:\\Users\\********\\"), // windows - RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), - "C:/Users/********/"), // windows with forward slashes - RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN - RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), - "new refresh token: \"\""), // refresh token - RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), - "\"device_code\" : \"\""), // device code -}; - -void anonymizeLog(QString& log) -{ - for (auto rule : anonymizeRules) { - log.replace(rule.reg, rule.with); - } -} diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp deleted file mode 100644 index bab4b9b9f..000000000 --- a/launcher/logs/LogParser.cpp +++ /dev/null @@ -1,359 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "LogParser.h" - -#include -#include "MessageLevel.h" - -using namespace Qt::Literals::StringLiterals; - -void LogParser::appendLine(QAnyStringView data) -{ - if (!m_partialData.isEmpty()) { - m_buffer = QString(m_partialData); - m_buffer.append("\n"); - m_partialData.clear(); - } - m_buffer.append(data.toString()); -} - -std::optional LogParser::getError() -{ - return m_error; -} - -std::optional LogParser::parseAttributes() -{ - LogParser::LogEntry entry{ - "", - MessageLevel::Info, - }; - auto attributes = m_parser.attributes(); - - for (const auto& attr : attributes) { - auto name = attr.name(); - auto value = attr.value(); - if (name == "logger"_L1) { - entry.logger = value.trimmed().toString(); - } else if (name == "timestamp"_L1) { - if (value.trimmed().isEmpty()) { - m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); - return {}; - } - entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); - } else if (name == "level"_L1) { - entry.levelText = value.trimmed().toString(); - entry.level = MessageLevel::fromName(entry.levelText); - } else if (name == "thread"_L1) { - entry.thread = value.trimmed().toString(); - } - } - if (entry.logger.isEmpty()) { - m_parser.raiseError("log4j:Event Missing required attribute: logger"); - return {}; - } - - return entry; -} - -void LogParser::setError() -{ - m_error = { - m_parser.errorString(), - m_parser.error(), - }; -} - -void LogParser::clearError() -{ - m_error = {}; // clear previous error -} - -bool isPotentialLog4JStart(QStringView buffer) -{ - static QString target = QStringLiteral(" LogParser::parseNext() -{ - clearError(); - - if (m_buffer.isEmpty()) { - return {}; - } - - if (m_buffer.trimmed().isEmpty()) { - auto text = QString(m_buffer); - m_buffer.clear(); - return LogParser::PlainText{ text }; - } - - // check if we have a full xml log4j event - bool isCompleteLog4j = false; - m_parser.clear(); - m_parser.setNamespaceProcessing(false); - m_parser.addData(m_buffer); - if (m_parser.readNextStartElement()) { - if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { - int depth = 1; - bool eod = false; - while (depth > 0 && !eod) { - auto tok = m_parser.readNext(); - switch (tok) { - case QXmlStreamReader::TokenType::StartElement: { - depth += 1; - } break; - case QXmlStreamReader::TokenType::EndElement: { - depth -= 1; - } break; - case QXmlStreamReader::TokenType::EndDocument: { - eod = true; // break outer while loop - } break; - default: { - // no op - } - } - if (m_parser.hasError()) { - break; - } - } - - isCompleteLog4j = depth == 0; - } - } - - if (isCompleteLog4j) { - return parseLog4J(); - } else { - if (isPotentialLog4JStart(m_buffer)) { - m_partialData = QString(m_buffer); - return LogParser::Partial{ QString(m_buffer) }; - } - - int start = 0; - auto bufView = QStringView(m_buffer); - while (start < bufView.length()) { - if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { - auto slicestart = start + pos; - auto slice = bufView.right(bufView.length() - slicestart); - if (isPotentialLog4JStart(slice)) { - if (slicestart > 0) { - auto text = m_buffer.left(slicestart); - m_buffer = m_buffer.right(m_buffer.length() - slicestart); - if (!text.trimmed().isEmpty()) { - return LogParser::PlainText{ text }; - } - } - m_partialData = QString(m_buffer); - return LogParser::Partial{ QString(m_buffer) }; - } - start = slicestart + 1; - } else { - break; - } - } - - // no log4j found, all plain text - auto text = QString(m_buffer); - m_buffer.clear(); - return LogParser::PlainText{ text }; - } -} - -QList LogParser::parseAvailable() -{ - QList items; - bool doNext = true; - while (doNext) { - auto item_ = parseNext(); - if (m_error.has_value()) { - return {}; - } - if (item_.has_value()) { - auto item = item_.value(); - if (std::holds_alternative(item)) { - break; - } else { - items.push_back(item); - } - } else { - doNext = false; - } - } - return items; -} - -std::optional LogParser::parseLog4J() -{ - m_parser.clear(); - m_parser.setNamespaceProcessing(false); - m_parser.addData(m_buffer); - - m_parser.readNextStartElement(); - if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { - auto entry_ = parseAttributes(); - if (!entry_.has_value()) { - setError(); - return {}; - } - auto entry = entry_.value(); - - bool foundMessage = false; - int depth = 1; - - enum parseOp { noOp, entryReady, parseError }; - - auto foundStart = [&]() -> parseOp { - depth += 1; - if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { - QString message; - bool messageComplete = false; - - while (!messageComplete) { - auto tok = m_parser.readNext(); - - switch (tok) { - case QXmlStreamReader::TokenType::Characters: { - message.append(m_parser.text()); - } break; - case QXmlStreamReader::TokenType::EndElement: { - if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { - messageComplete = true; - } - } break; - case QXmlStreamReader::TokenType::EndDocument: { - return parseError; // parse fail - } break; - default: { - // no op - } - } - - if (m_parser.hasError()) { - return parseError; - } - } - - entry.message = message; - foundMessage = true; - depth -= 1; - } - return noOp; - }; - - auto foundEnd = [&]() -> parseOp { - depth -= 1; - if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { - if (foundMessage) { - auto consumed = m_parser.characterOffset(); - if (consumed > 0 && consumed <= m_buffer.length()) { - m_buffer = m_buffer.right(m_buffer.length() - consumed); - // potential whitespace preserved for next item - } - clearError(); - return entryReady; - } - m_parser.raiseError("log4j:Event Missing required attribute: message"); - setError(); - return parseError; - } - return noOp; - }; - - while (!m_parser.atEnd()) { - auto tok = m_parser.readNext(); - parseOp op = noOp; - switch (tok) { - case QXmlStreamReader::TokenType::StartElement: { - op = foundStart(); - } break; - case QXmlStreamReader::TokenType::EndElement: { - op = foundEnd(); - } break; - case QXmlStreamReader::TokenType::EndDocument: { - return {}; - } break; - default: { - // no op - } - } - - switch (op) { - case parseError: - return {}; // parse fail or error - case entryReady: - return entry; - case noOp: - default: { - // no op - } - } - - if (m_parser.hasError()) { - return {}; - } - } - } - - throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); -} - -MessageLevel LogParser::guessLevel(const QString& line, MessageLevel previous) -{ - static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - auto match = LINE_WITH_LEVEL.match(line); - if (match.hasMatch()) { - // New style logs from log4j - QString timestamp = match.captured("timestamp"); - QString levelStr = match.captured("level"); - - return MessageLevel::fromName(levelStr); - } else { - // Old style forge logs - if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || - line.contains("[FINEST]")) - return MessageLevel::Info; - if (line.contains("[SEVERE]") || line.contains("[STDERR]")) - return MessageLevel::Error; - if (line.contains("[WARNING]")) - return MessageLevel::Warning; - if (line.contains("[DEBUG]")) - return MessageLevel::Debug; - } - - if (line.contains("Exception: ") || line.contains("Throwable: ")) - return MessageLevel::Error; - - if (line.startsWith("Caused by: ") || line.startsWith("Exception in thread")) - return MessageLevel::Error; - - if (line.contains("overwriting existing")) - return MessageLevel::Fatal; - - if (line.startsWith("\t") || line.startsWith(" ")) - return previous; - - return MessageLevel::Unknown; -} diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h deleted file mode 100644 index ae657297c..000000000 --- a/launcher/logs/LogParser.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include "MessageLevel.h" - -class LogParser { - public: - struct LogEntry { - QString logger; - MessageLevel level; - QString levelText; - QDateTime timestamp; - QString thread; - QString message; - }; - struct Partial { - QString data; - }; - struct PlainText { - QString message; - }; - struct Error { - QString errMessage; - QXmlStreamReader::Error error; - }; - - using ParsedItem = std::variant; - - public: - LogParser() = default; - - void appendLine(QAnyStringView data); - std::optional parseNext(); - QList parseAvailable(); - std::optional getError(); - - /// guess log level from a line of game log - static MessageLevel guessLevel(const QString& line, MessageLevel previous); - - protected: - std::optional parseAttributes(); - void setError(); - void clearError(); - - std::optional parseLog4J(); - - private: - QString m_buffer; - QString m_partialData; - QXmlStreamReader m_parser; - std::optional m_error; -}; diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.h b/launcher/macsandbox/SecurityBookmarkFileAccess.h deleted file mode 100644 index 69b344af9..000000000 --- a/launcher/macsandbox/SecurityBookmarkFileAccess.h +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef FILEACCESS_H -#define FILEACCESS_H - -#include -#include -Q_FORWARD_DECLARE_OBJC_CLASS(NSData); -Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); -Q_FORWARD_DECLARE_OBJC_CLASS(NSString); -Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool); -Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary); -Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet); -class QString; -class QByteArray; -class QUrl; - -class SecurityBookmarkFileAccess { - /// The keys are bookmarks and the values are URLs. - NSMutableDictionary* m_bookmarks; - /// The keys are paths and the values are bookmarks. - NSMutableDictionary* m_paths; - /// Contains URLs that are currently being accessed. - NSMutableSet* m_activeURLs; - - bool m_readOnly; - - NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale); - - public: - /// \param readOnly A boolean indicating whether the bookmark should be read-only. - SecurityBookmarkFileAccess(bool readOnly = false); - ~SecurityBookmarkFileAccess(); - - /// Get a security scoped bookmark from a URL. - /// - /// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling - /// this function. Note that this is called implicitly if the user selects the directory from a file picker. - /// \param url The URL to get the security scoped bookmark from. - /// \return A QByteArray containing the security scoped bookmark. - QByteArray urlToSecurityScopedBookmark(const QUrl& url); - /// Get a security scoped bookmark from a path. - /// - /// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling - /// this function. Note that this is called implicitly if the user selects the directory from a file picker. - /// \param path The path to get the security scoped bookmark from. - /// \return A QByteArray containing the security scoped bookmark. - QByteArray pathToSecurityScopedBookmark(const QString& path); - /// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated. - /// - /// You must check whether the URL is valid before using it. - /// \param bookmark The security scoped bookmark to get the URL from. - /// \param isStale A boolean that will be set to true if the bookmark is stale. - /// \return The URL from the security scoped bookmark. - QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale); - - /// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this - /// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has - /// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that - /// this is the only `SecurityBookmarkFileAccess` accessing the resource. - /// - /// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be - /// updated. - /// \param bookmark The security scoped bookmark to start accessing. - /// \param isStale A boolean that will be set to true if the bookmark is stale. - /// \return A boolean indicating whether the bookmark was successfully accessed. - bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale); - void stopUsingSecurityScopedBookmark(QByteArray& bookmark); - - /// Returns true if access to the `path` is currently being maintained by this object. - bool isAccessingPath(const QString& path); -}; - -#endif // FILEACCESS_H diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.mm b/launcher/macsandbox/SecurityBookmarkFileAccess.mm deleted file mode 100644 index bee854abe..000000000 --- a/launcher/macsandbox/SecurityBookmarkFileAccess.mm +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "SecurityBookmarkFileAccess.h" - -#include -#include -#include - -QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url) -{ - if (!url.isLocalFile()) - return {}; - - NSError* error = nil; - NSURL* nsurl = [url.toNSURL() absoluteURL]; - NSData* bookmark; - if ([m_paths objectForKey:[nsurl path]]) { - bookmark = m_paths[[nsurl path]]; - } else { - bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | - (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) - includingResourceValuesForKeys:nil - relativeToURL:nil - error:&error]; - } - if (error) { - return {}; - } - - // remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks - // sometimes you need to call this twice to actually stop access (extra calls aren't harmful) - [nsurl stopAccessingSecurityScopedResource]; - [nsurl stopAccessingSecurityScopedResource]; - nsurl = [NSURL URLByResolvingBookmarkData:bookmark - options:NSURLBookmarkResolutionWithSecurityScope | - (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) - relativeToURL:nil - bookmarkDataIsStale:nil - error:&error]; - m_paths[[nsurl path]] = bookmark; - m_bookmarks[bookmark] = nsurl; - - QByteArray qBookmark = QByteArray::fromNSData(bookmark); - bool isStale = false; - startUsingSecurityScopedBookmark(qBookmark, isStale); - - return qBookmark; -} - -SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly) -{ - m_bookmarks = [NSMutableDictionary new]; - m_paths = [NSMutableDictionary new]; - m_activeURLs = [NSMutableSet new]; -} - -SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess() -{ - for (NSURL* url : m_activeURLs) { - [url stopAccessingSecurityScopedResource]; - } -} - -QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path) -{ - return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path)); -} - -NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale) -{ - NSError* error = nil; - BOOL localStale = NO; - NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() - options:NSURLBookmarkResolutionWithSecurityScope | - (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) - relativeToURL:nil - bookmarkDataIsStale:&localStale - error:&error]; - if (error) { - return nil; - } - isStale = localStale; - if (isStale) { - NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | - (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) - includingResourceValuesForKeys:nil - relativeToURL:nil - error:&error]; - if (error) { - return nil; - } - bookmark = QByteArray::fromNSData(nsBookmark); - } - - NSData* nsBookmark = bookmark.toNSData(); - m_paths[[nsurl path]] = nsBookmark; - m_bookmarks[nsBookmark] = nsurl; - - return nsurl; -} - -QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale) -{ - if (bookmark.isEmpty()) - return {}; - - NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale); - if (!url) - return {}; - - return QUrl::fromNSURL(url); -} - -bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale) -{ - NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] - : securityScopedBookmarkToNSURL(bookmark, isStale); - if ([m_activeURLs containsObject:url]) - return false; - - [url stopAccessingSecurityScopedResource]; - if ([url startAccessingSecurityScopedResource]) { - [m_activeURLs addObject:url]; - return true; - } - return false; -} - -void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark) -{ - if (![m_bookmarks objectForKey:bookmark.toNSData()]) - return; - NSURL* url = m_bookmarks[bookmark.toNSData()]; - - if ([m_activeURLs containsObject:url]) { - [url stopAccessingSecurityScopedResource]; - [url stopAccessingSecurityScopedResource]; - - [m_activeURLs removeObject:url]; - [m_paths removeObjectForKey:[url path]]; - [m_bookmarks removeObjectForKey:bookmark.toNSData()]; - } -} - -bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path) -{ - NSData* bookmark = [m_paths objectForKey:path.toNSString()]; - if (!bookmark && path.endsWith('/')) { - bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()]; - } - if (!bookmark) { - return false; - } - NSURL* url = [m_bookmarks objectForKey:bookmark]; - return [m_activeURLs containsObject:url]; -} diff --git a/launcher/main.cpp b/launcher/main.cpp index 9378387bb..35f545f52 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -33,23 +33,39 @@ * limitations under the License. */ -#include - #include "Application.h" -#if defined Q_OS_WIN32 -#include "console/WindowsConsole.h" +// #define BREAK_INFINITE_LOOP +// #define BREAK_EXCEPTION +// #define BREAK_RETURN + +#ifdef BREAK_INFINITE_LOOP +#include +#include #endif int main(int argc, char* argv[]) { -#if defined Q_OS_WIN32 - // used on Windows to attach the standard IO streams - console::WindowsConsoleGuard _consoleGuard; +#ifdef BREAK_INFINITE_LOOP + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } +#endif +#ifdef BREAK_EXCEPTION + throw 42; +#endif +#ifdef BREAK_RETURN + return 42; +#endif + +#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // initialize Qt Application app(argc, argv); + switch (app.status()) { case Application::StartingUp: case Application::Initialized: { @@ -68,8 +84,6 @@ int main(int argc, char* argv[]) Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); - - Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 1869e14d3..b0e754ada 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -26,7 +26,6 @@ #include "net/NetJob.h" #include "Application.h" -#include "settings/SettingsObject.h" #include "BuildConfig.h" #include "tasks/Task.h" @@ -82,12 +81,12 @@ QUrl BaseEntity::url() const return QUrl(metaOverride).resolved(localFilename()); } -Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload) +Task::Ptr BaseEntity::loadTask(Net::Mode mode) { if (m_task && m_task->isRunning()) { return m_task; } - m_task.reset(new BaseEntityLoadTask(this, mode, forceReload)); + m_task.reset(new BaseEntityLoadTask(this, mode)); return m_task; } @@ -107,9 +106,7 @@ BaseEntity::LoadStatus BaseEntity::status() const return m_load_status; } -BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload) - : m_entity(parent), m_mode(mode), m_force_reload(forceReload) -{} +BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} void BaseEntityLoadTask::executeTask() { @@ -127,11 +124,9 @@ void BaseEntityLoadTask::executeTask() } // on online the hash needs to match - const auto& expected = m_entity->m_sha256; - const auto& actual = m_entity->m_file_sha256; - hashMatches = expected == actual; + hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { - throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual)); + throw Exception("mismatched checksum"); } // load local file @@ -153,18 +148,13 @@ 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 && !m_force_reload)) { + if (wasLoadedOffline || wasLoadedRemote) { 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 32d8bdbb8..17aa0cb87 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, bool forceReload = false); + [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); 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, bool forceReload); + explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); ~BaseEntityLoadTask() override = default; virtual void executeTask() override; @@ -68,7 +68,6 @@ 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 bd58215c4..1707854be 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -15,7 +15,6 @@ #include "Index.h" -#include "Application.h" #include "JsonFormat.h" #include "QObjectPtr.h" #include "VersionList.h" @@ -24,7 +23,7 @@ namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} -Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) +Index::Index(const QVector& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); @@ -104,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QList lists = other->m_lists; + const QVector lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); @@ -136,7 +135,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 || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) { + if (mode == Net::Mode::Offline) { return get(uid, version)->loadTask(mode); } @@ -155,7 +154,7 @@ Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) { QEventLoop ev; auto task = loadVersion(uid, version); - connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); return get(uid, version); diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index fe5bf2170..026a00c07 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); - explicit Index(const QList& lists, QObject* parent = nullptr); + explicit Index(const QVector& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -46,7 +46,7 @@ class Index : public QAbstractListModel, public BaseEntity { Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; - QList lists() const { return m_lists; } + QVector lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); @@ -60,7 +60,7 @@ class Index : public QAbstractListModel, public BaseEntity { void parse(const QJsonObject& obj) override; private: - QList m_lists; + QVector m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index db1947655..86af7277e 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -35,13 +35,13 @@ MetadataVersion currentFormatVersion() // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { - const QList objects = requireIsArrayOf(obj, "packages"); - QList lists; + const QVector objects = requireIsArrayOf(obj, "packages"); + QVector lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); - list->setName(obj["name"].toString()); - list->setSha256(obj["sha256"].toString()); + list->setName(ensureString(obj, "name", QString())); + list->setSha256(ensureString(obj, "sha256", QString())); return list; }); return std::make_shared(lists); @@ -52,14 +52,14 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob { Version::Ptr version = std::make_shared(uid, requireString(obj, "version")); version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); - version->setType(obj["type"].toString()); - version->setRecommended(obj["recommended"].toBool()); - version->setVolatile(obj["volatile"].toBool()); + version->setType(ensureString(obj, "type", QString())); + version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); + version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); RequireSet reqs, conflicts; parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); - if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) { + if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) { version->setSha256(sha256); } return version; @@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); - const QList versionsRaw = requireIsArrayOf(obj, "versions"); - QList versions; + const QVector versionsRaw = requireIsArrayOf(obj, "versions"); + QVector versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); @@ -89,7 +89,7 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) }); VersionList::Ptr list = std::make_shared(uid); - list->setName(obj["name"].toString()); + list->setName(ensureString(obj, "name", QString())); list->setVersions(versions); return list; } @@ -171,8 +171,8 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName) while (iter != reqArray.end()) { auto reqObject = requireObject(*iter); auto uid = requireString(reqObject, "uid"); - auto equals = reqObject["equals"].toString(); - auto suggests = reqObject["suggests"].toString(); + auto equals = ensureString(reqObject, "equals", QString()); + auto suggests = ensureString(reqObject, "suggests", QString()); ptr->insert({ uid, equals, suggests }); iter++; } diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index ce9a9cc8a..74e71e91c 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -21,11 +21,11 @@ Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} -QString Meta::Version::descriptor() const +QString Meta::Version::descriptor() { return m_version; } -QString Meta::Version::name() const +QString Meta::Version::name() { if (m_data) return m_data->name; @@ -88,7 +88,7 @@ QString Meta::Version::localFilename() const ::Version Meta::Version::toComparableVersion() const { - return { descriptor() }; + return { const_cast(this)->descriptor() }; } void Meta::Version::setType(const QString& type) diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index a2bbc6176..46dc740da 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -19,8 +19,8 @@ #include "BaseVersion.h" #include -#include #include +#include #include #include "minecraft/VersionFile.h" @@ -40,8 +40,8 @@ class Version : public QObject, public BaseVersion, public BaseEntity { explicit Version(const QString& uid, const QString& version); virtual ~Version() = default; - QString descriptor() const override; - QString name() const override; + QString descriptor() override; + QString name() override; QString typeString() const override; QString uid() const { return m_uid; } @@ -60,7 +60,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { QString localFilename() const override; - ::Version toComparableVersion() const; + [[nodiscard]] ::Version toComparableVersion() const; public: // for usage by format parsers only void setType(const QString& type); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index fa3a271a6..f96355658 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(bool forceReload) +Task::Ptr VersionList::getLoadTask() { 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, forceReload)); - loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload)); + loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); + loadTask->addTask(this->loadTask(Net::Mode::Online)); return loadTask; } @@ -158,8 +158,8 @@ Version::Ptr VersionList::getVersion(const QString& version) bool VersionList::hasVersion(QString version) const { - auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), - [version](Meta::Version::Ptr const& a) { return a->version() == version; }); + auto ver = + std::find_if(m_versions.constBegin(), m_versions.constEnd(), [&](Meta::Version::Ptr const& a) { return a->version() == version; }); return (ver != m_versions.constEnd()); } @@ -169,7 +169,7 @@ void VersionList::setName(const QString& name) emit nameChanged(name); } -void VersionList::setVersions(const QList& versions) +void VersionList::setVersions(const QVector& versions) { beginResetModel(); m_versions = versions; @@ -265,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); + [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); @@ -282,7 +282,7 @@ void VersionList::waitToLoad() return; QEventLoop ev; auto task = getLoadTask(); - connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); } diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 709c361de..4215439db 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(bool forceReload = false) override; + [[nodiscard]] Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; @@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity { Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; - QList versions() const { return m_versions; } + QVector versions() const { return m_versions; } // this blocks until the version list is loaded void waitToLoad(); public: // for usage only by parsers void setName(const QString& name); - void setVersions(const QList& versions); + void setVersions(const QVector& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; @@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity { void updateListData(QList) override {} private: - QList m_versions; + QVector m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; diff --git a/launcher/minecraft/Agent.h b/launcher/minecraft/Agent.h index 2432679da..bc385a74e 100644 --- a/launcher/minecraft/Agent.h +++ b/launcher/minecraft/Agent.h @@ -4,10 +4,26 @@ #include "Library.h" -struct Agent { +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 */ /// The library pointing to the jar this Java agent is contained within - LibraryPtr library; + LibraryPtr m_library; /// The argument to the Java agent, passed after an = if present - QString argument; + QString m_argument; }; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 37d02b5c1..4406d9b34 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -52,7 +52,6 @@ #include "Application.h" #include "net/NetRequest.h" -#include "update/AssetUpdateTask.h" namespace { QSet collectPathsFromDir(QString dirPath) @@ -104,7 +103,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 << "error:" << file.errorString(); + qCritical() << "Failed to read assets index file" << path; return false; } index.id = assetsId; @@ -160,7 +159,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd if (key == "hash") { object.hash = value.toString(); } else if (key == "size") { - object.size = value.toLongLong(); + object.size = value.toDouble(); } } @@ -299,8 +298,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - auto resourceURL = AssetUpdateTask::resourceUrl(); - return resourceURL + getRelPath(); + return BuildConfig.RESOURCE_BASE + getRelPath(); } QString AssetObject::getRelPath() diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 6a8bb27c0..ad7ef545c 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -462,7 +462,7 @@ void Component::waitLoadMeta() } } -void Component::setUpdateAction(const UpdateAction& action) +void Component::setUpdateAction(UpdateAction action) { m_updateAction = action; } @@ -479,7 +479,6 @@ void Component::clearUpdateAction() QDebug operator<<(QDebug d, const Component& comp) { - QDebugStateSaver saver(d); - d.nospace() << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; return d; } diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index eafdb8ed7..203cc2241 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -106,7 +106,7 @@ class Component : public QObject, public ProblemProvider { void waitLoadMeta(); - void setUpdateAction(const UpdateAction& action); + void setUpdateAction(UpdateAction action); void clearUpdateAction(); UpdateAction getUpdateAction(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 73203f74b..36a07ee72 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -38,9 +38,6 @@ * 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); @@ -51,38 +48,9 @@ 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(); } @@ -228,13 +196,12 @@ 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); - return; + break; } case LoadResult::RequiresRemote: { // we wait for signals. @@ -242,11 +209,9 @@ void ComponentUpdateTask::loadComponents() } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); - return; + break; } } - - setDetails(tr("Downloading metadata for %1 components").arg(taskIndex)); } namespace { @@ -556,7 +521,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) // change a version of something that exists for (auto& change : toChange) { // FIXME: this should not work directly with the component list - 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); } @@ -605,7 +570,7 @@ void ComponentUpdateTask::performUpdateActions() component->setVersion(cv.targetVersion); component->waitLoadMeta(); }, - [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) { + [&component, &instance](const UpdateActionLatestRecommendedCompatible lrc) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() @@ -779,17 +744,17 @@ 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; + taskSlot.error = msg; d->remoteTasksInProgress--; checkIfAllFinished(); } void ComponentUpdateTask::checkIfAllFinished() { - setProgress(m_progress + 1, m_progressTotal); if (d->remoteTasksInProgress) { // not yet... return; @@ -804,14 +769,11 @@ void ComponentUpdateTask::checkIfAllFinished() QStringList allErrorsList; for (auto& item : d->remoteLoadStatusList) { if (!item.succeeded) { - const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); - allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.") - .arg(component->getName(), component->m_version)); + allErrorsList.append(item.error); } } - 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 2ef9737ba..64c55877b 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -17,12 +17,8 @@ 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() override; + void executeTask(); private: void loadComponents(); diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index 8ffb9c71e..2fc0b6d9a 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -15,6 +15,7 @@ struct RemoteLoadStatus { size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; + QString error; Task::Ptr task; }; diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 65297abed..22db7d641 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -38,10 +38,12 @@ #include #include #include +#include "DefaultVariable.h" struct GradleSpecifier { GradleSpecifier() { m_valid = false; } - GradleSpecifier(const QString& value) + GradleSpecifier(QString value) { operator=(value); } + GradleSpecifier& operator=(const QString& value) { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar @@ -52,15 +54,15 @@ struct GradleSpecifier { 4 "jdk15" 5 "jar" */ - static const QRegularExpression s_matcher( + QRegularExpression matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); - QRegularExpressionMatch match = s_matcher.match(value); + QRegularExpressionMatch match = matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; - return; + return *this; } auto elements = match.captured(); m_groupId = match.captured(1); @@ -70,6 +72,7 @@ struct GradleSpecifier { if (match.lastCapturedIndex() >= 5) { m_extension = match.captured(5); } + return *this; } QString serialize() const { @@ -80,8 +83,8 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { retval += ":" + m_classifier; } - if (m_extension.has_value()) { - retval += "@" + m_extension.value(); + if (m_extension.isExplicit()) { + retval += "@" + m_extension; } return retval; } @@ -94,7 +97,7 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { filename += "-" + m_classifier; } - filename += "." + m_extension.value_or("jar"); + filename += "." + m_extension; return filename; } QString toPath(const QString& filenameOverride = QString()) const @@ -119,13 +122,26 @@ 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 std::optional extension() const { return m_extension; } + inline QString 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 = default; + 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; + } private: QString m_invalidValue; @@ -133,6 +149,6 @@ struct GradleSpecifier { QString m_artifactId; QString m_version; QString m_classifier; - std::optional m_extension; + DefaultVariable m_extension = DefaultVariable("jar"); bool m_valid = false; }; diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index fb74d4a9a..c11a0f915 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(const Agent& agent, const RuntimeContext& runtimeContext) +void LaunchProfile::applyAgent(AgentPtr 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,8 +349,7 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath, - bool addJarMods) const + const QString& tempPath) const { QStringList native32, native64; jars.clear(); @@ -361,7 +360,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() && addJarMods) { + if (m_jarMods.size()) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 6dc3d9aeb..f1be6fee0 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(const Agent& agent, const RuntimeContext& runtimeContext); + void applyAgent(AgentPtr 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,8 +87,7 @@ class LaunchProfile : public ProblemProvider { QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath, - bool addJarMods = true) const; + const QString& tempPath) const; bool hasTrait(const QString& trait) const; ProblemSeverity getProblemSeverity() const override; const QList getProblems() const override; @@ -133,7 +132,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 2b43d4389..4f04f0eb9 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -65,7 +65,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, { bool local = isLocal(); // Lambda function to get the absolute file path - auto actualPath = [this, local, overridePath](QString relPath) { + auto actualPath = [&](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { @@ -115,7 +115,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC bool local = isLocal(); // Lambda function to check if a local file exists - auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { + auto check_local_file = [&](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); @@ -128,7 +128,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC }; // Lambda function to add a download request - auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) { + auto add_download = [&](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); } @@ -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 << "expected sha1:" << sha1; + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { out.append(Net::ApiDownload::makeCached(url, entry, options)); @@ -198,7 +198,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC } } } else { - auto raw_dl = [this, raw_storage]() { + auto raw_dl = [&]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } @@ -242,13 +242,13 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const if (m_rules.empty()) { result = true; } else { - Rule::Action ruleResult = Rule::Disallow; + RuleAction ruleResult = Disallow; for (auto rule : m_rules) { - Rule::Action temp = rule.apply(runtimeContext); - if (temp != Rule::Defer) + RuleAction temp = rule->apply(this, runtimeContext); + if (temp != Defer) ruleResult = temp; } - result = result && (ruleResult == Rule::Allow); + result = result && (ruleResult == Allow); } if (isNative()) { result = result && !getCompatibleNative(runtimeContext).isNull(); diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index d827554aa..d3019e814 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -129,7 +129,7 @@ class Library { void setHint(const QString& hint) { m_hint = hint; } /// Set the load rules - void setRules(QList rules) { m_rules = rules; } + void setRules(QList> rules) { m_rules = rules; } /// Returns true if the library should be loaded (or extracted, in case of natives) bool isActive(const RuntimeContext& runtimeContext) const; @@ -203,7 +203,7 @@ class Library { bool applyRules = false; /// rules associated with the library - QList m_rules; + QList> m_rules; /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; diff --git a/launcher/minecraft/Logging.cpp b/launcher/minecraft/Logging.cpp index 8b6304205..92596de3e 100644 --- a/launcher/minecraft/Logging.cpp +++ b/launcher/minecraft/Logging.cpp @@ -19,6 +19,7 @@ */ #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 8e98a2efe..d6d45af6b 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -38,14 +38,22 @@ #include "MinecraftInstance.h" #include "Application.h" #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" #include "FileSystem.h" #include "MMCTime.h" #include "java/JavaVersion.h" +#include "pathmatcher/MultiMatcher.h" +#include "pathmatcher/RegexpMatcher.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" @@ -56,24 +64,13 @@ #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" @@ -88,62 +85,19 @@ #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" +#include "minecraft/gameoptions/GameOptions.h" +#include "minecraft/update/FoldersTask.h" #include "tools/BaseProfiler.h" #include -#include -#include -#include -#include #ifdef Q_OS_LINUX -#include "LibraryUtils.h" -#endif - -#ifdef WITH_QTDBUS -#include +#include "MangoHud.h" #endif #define IBUS "@im=ibus" -[[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) -{ -#ifdef WITH_QTDBUS - if (!QDBusConnection::systemBus().isConnected()) - return false; - - QDBusInterface switcheroo("net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", "org.freedesktop.DBus.Properties", - QDBusConnection::systemBus()); - - if (!switcheroo.isValid()) - return false; - - QDBusReply reply = - switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); - if (!reply.isValid()) - return false; - - QDBusArgument arg = qvariant_cast(reply.value().variant()); - QList gpus; - arg >> gpus; - - for (const auto& gpu : gpus) { - QString name = qvariant_cast(gpu[QStringLiteral("Name")]); - bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); - 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]); - } - return true; - } - } -#endif - return false; -} - // all of this because keeping things compatible with deprecated old settings // if either of the settings {a, b} is true, this also resolves to true class OrSetting : public Setting { @@ -164,14 +118,12 @@ class OrSetting : public Setting { std::shared_ptr m_b; }; -MinecraftInstance::MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) - : BaseInstance(globalSettings, std::move(settings), rootDir) +MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + : BaseInstance(globalSettings, settings, rootDir) { m_components.reset(new PackProfile(this)); } -MinecraftInstance::~MinecraftInstance() {} - void MinecraftInstance::saveNow() { m_components->saveNow(); @@ -210,7 +162,6 @@ 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); @@ -255,17 +206,6 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); - m_settings->registerSetting("ExportRecommendedRAM"); - - auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); - auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); - - connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); - connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); - - // Join server on launch, this does not have a global override - m_settings->registerSetting("OverrideModDownloadLoaders", false); - m_settings->registerSetting("ModDownloadLoaders", "[]"); qDebug() << "Instance-type specific settings were loaded!"; @@ -276,7 +216,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { - m_runtimeContext.updateFromInstanceSettings(m_settings.get()); + m_runtimeContext.updateFromInstanceSettings(m_settings); m_components->invalidateLaunchProfile(); } @@ -285,9 +225,9 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -PackProfile* MinecraftInstance::getPackProfile() const +std::shared_ptr MinecraftInstance::getPackProfile() const { - return m_components.get(); + return m_components; } QSet MinecraftInstance::traits() const @@ -315,9 +255,9 @@ void MinecraftInstance::populateLaunchMenu(QMenu* menu) normalLaunchDemo->setEnabled(supportsDemo()); - 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); }); + 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); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); @@ -409,16 +349,6 @@ QString MinecraftInstance::nilModsDir() const return FS::PathCombine(gameRoot(), "nilmods"); } -QString MinecraftInstance::dataPacksDir() -{ - QString relativePath = settings()->get("GlobalDataPacksPath").toString(); - - if (relativePath.isEmpty()) - relativePath = "datapacks"; - - return QDir(gameRoot()).filePath(relativePath); -} - QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); @@ -491,28 +421,6 @@ QStringList MinecraftInstance::getNativeJars() return nativeJars; } -static QString replaceTokensIn(const QString& text, const QMap& with) -{ - // TODO: does this still work?? - QString result; - static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); - QStringList list; - QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); - int lastCapturedEnd = 0; - while (i.hasNext()) { - QRegularExpressionMatch match = i.next(); - result.append(text.mid(lastCapturedEnd, match.capturedStart())); - QString key = match.captured(1); - auto iter = with.find(key); - if (iter != with.end()) { - result.append(*iter); - } - lastCapturedEnd = match.capturedEnd(); - } - result.append(text.mid(lastCapturedEnd)); - return result; -} - QStringList MinecraftInstance::extraArguments() { auto list = BaseInstance::extraArguments(); @@ -525,17 +433,13 @@ QStringList MinecraftInstance::extraArguments() } auto addn = m_components->getProfile()->getAddnJvmArguments(); if (!addn.isEmpty()) { - QMap tokenMapping = makeProfileVarMapping(m_components->getProfile()); - - for (const QString& item : addn) { - list.append(replaceTokensIn(item, tokenMapping)); - } + list.append(addn); } auto agents = m_components->getProfile()->getAgents(); - for (const auto& agent : agents) { + for (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())); } { @@ -571,8 +475,6 @@ 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()); @@ -597,16 +499,6 @@ QStringList MinecraftInstance::javaArguments() "minecraft.exe.heapdump"); #endif - // LWJGL2 reads `LWJGL_DISABLE_XRANDR` to force disable xrandr usage and fall back to xf86videomode. - // It *SHOULD* check for the executable to exist before trying to use it for queries but it doesnt, - // so WE can and force disable xrandr if it is not available. -#ifdef Q_OS_LINUX - // LWJGL2 is "org.lwjgl" LWJGL3 is "org.lwjgl3" - if (m_components->getComponent("org.lwjgl") != nullptr && QStandardPaths::findExecutable("xrandr").isEmpty()) { - args << QString("-DLWJGL_DISABLE_XRANDR=true"); - } -#endif - int min = settings()->get("MinMemAlloc").toInt(); int max = settings()->get("MaxMemAlloc").toInt(); if (min < max) { @@ -626,6 +518,8 @@ 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"; @@ -636,7 +530,7 @@ QStringList MinecraftInstance::javaArguments() QString MinecraftInstance::getLauncher() { // use legacy launcher if the traits are set - if (isLegacy()) + if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch")) return "legacy"; return "standard"; @@ -654,16 +548,9 @@ 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", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); + out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); -#ifdef Q_OS_MACOS - // get library for Steam overlay support - QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); - if (!steamDyldInsertLibraries.isEmpty()) { - out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); - } -#endif return out; } @@ -679,8 +566,7 @@ QProcessEnvironment MinecraftInstance::createEnvironment() } // custom env - auto insertEnv = [&env](QString value) { - auto envMap = Json::toMap(value); + auto insertEnv = [&env](QMap envMap) { if (envMap.isEmpty()) return; @@ -688,7 +574,12 @@ QProcessEnvironment MinecraftInstance::createEnvironment() env.insert(iter.key(), iter.value().toString()); }; - insertEnv(settings()->get("Env").toString()); + bool overrideEnv = settings()->get("OverrideEnv").toBool(); + + if (!overrideEnv) + insertEnv(APPLICATION->settings()->get("Env").toMap()); + else + insertEnv(settings()->get("Env").toMap()); return env; } @@ -703,7 +594,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); - auto mangoHudLibString = LibraryUtils::findMangoHud(); + auto mangoHudLibString = MangoHud::getLibraryString(); if (!mangoHudLibString.isEmpty()) { QFileInfo mangoHudLib(mangoHudLibString); QString libPath = mangoHudLib.absolutePath(); @@ -715,7 +606,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() // dlsym variant is only needed for OpenGL and not included in the vulkan layer appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_opengl.so"); - appendLib("libMangoHud_shim.so"); preloadList << mangoHudLibString; } @@ -724,14 +614,12 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() } if (settings()->get("UseDiscreteGpu").toBool()) { - if (!switcherooSetupGPU(env)) { - // Open Source Drivers - env.insert("DRI_PRIME", "1"); - // Proprietary Nvidia Drivers - env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); - env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); - env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); - } + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); } if (settings()->get("UseZink").toBool()) { @@ -739,54 +627,92 @@ 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; } +static QString replaceTokensIn(QString text, QMap with) +{ + // TODO: does this still work?? + QString result; + QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + QStringList list; + QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); + int lastCapturedEnd = 0; + while (i.hasNext()) { + QRegularExpressionMatch match = i.next(); + result.append(text.mid(lastCapturedEnd, match.capturedStart())); + QString key = match.captured(1); + auto iter = with.find(key); + if (iter != with.end()) { + result.append(*iter); + } + lastCapturedEnd = match.capturedEnd(); + } + result.append(text.mid(lastCapturedEnd)); + return result; +} + QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); - auto args = profile->getMinecraftArguments().split(' ', Qt::SkipEmptyParts); + QString args_pattern = profile->getMinecraftArguments(); for (auto tweaker : profile->getTweakers()) { - args << "--tweakClass" << tweaker; + args_pattern += " --tweakClass " + tweaker; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { if (profile->hasTrait("feature:is_quick_play_multiplayer")) { - args << "--quickPlayMultiplayer" << targetToJoin->address + ':' + QString::number(targetToJoin->port); + args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); } else { - args << "--server" << targetToJoin->address; - args << "--port" << QString::number(targetToJoin->port); + args_pattern += " --server " + targetToJoin->address; + args_pattern += " --port " + QString::number(targetToJoin->port); } } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { - args << "--quickPlaySingleplayer" << targetToJoin->world; + args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; } } - QMap tokenMapping = makeProfileVarMapping(profile); - + QMap token_mapping; // yggdrasil! if (session) { // token_mapping["auth_username"] = session->username; - tokenMapping["auth_session"] = session->session; - tokenMapping["auth_access_token"] = session->access_token; - tokenMapping["auth_player_name"] = session->player_name; - tokenMapping["auth_uuid"] = session->uuid; - tokenMapping["user_properties"] = session->serializeUserProperties(); - tokenMapping["user_type"] = session->user_type; - - if (session->launchMode == LaunchMode::Demo) { - args << "--demo"; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + if (session->demo) { + args_pattern += " --demo"; } } - for (int i = 0; i < args.length(); i++) { - args[i] = replaceTokensIn(args[i], tokenMapping); + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); + token_mapping["version_type"] = profile->getMinecraftVersionType(); + + QString absRootDir = QDir(gameRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = profile->getMinecraftAssets(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); + + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = assets->id; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); +#else + QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); +#endif + for (int i = 0; i < parts.length(); i++) { + parts[i] = replaceTokensIn(parts[i], token_mapping); } - return args; + return parts; } QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) @@ -826,30 +752,11 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT // window size, title and state, legacy { QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) { - // FIXME doesn't support maximisation - if (!isLegacy()) { - auto screen = QGuiApplication::primaryScreen(); - auto screenGeometry = screen->availableSize(); - - // small hack to get the widow decorations - for (auto w : QApplication::topLevelWidgets()) { - auto mainWindow = qobject_cast(w); - if (mainWindow) { - auto m = mainWindow->windowHandle()->frameMargins(); - screenGeometry = screenGeometry.shrunkBy(m); - break; - } - } - - windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); - } else { - windowParams = "maximized"; - } - } else { + if (settings()->get("LaunchMaximized").toBool()) + windowParams = "maximized"; + else windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); - } launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; } @@ -888,26 +795,60 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { - constexpr auto indent = " "; - constexpr auto emptyLine = ""; - QStringList out; - - 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; + out << "Main Class:" << " " + getMainClass() << ""; + out << "Native path:" << " " + getNativePath() << ""; 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 = [&](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) { + auto printModList = [&](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); @@ -928,12 +869,12 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } - out << emptyLine; + out << ""; } }; - printModList("Mods", *loaderModList()); - printModList("Core Mods", *coreModList()); + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); // jar mods auto& jarMods = profile->getJarMods(); @@ -943,59 +884,19 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto displayname = jarmod->displayName(runtimeContext()); auto realname = jarmod->filename(runtimeContext()); if (displayname != realname) { - out << indent + displayname + " (" + realname + ")"; + out << " " + displayname + " (" + realname + ")"; } else { - out << indent + realname; + out << " " + realname; } } - out << emptyLine; + out << ""; } - // 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 << "Minecraft arguments:"; - out << indent + params.join(' '); - out << emptyLine; + out << "Params:"; + out << " " + params.join(' '); + out << ""; // window size QString windowParams; @@ -1006,18 +907,9 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } - 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; - } - + out << ""; + out << "Launcher: " + getLauncher(); + out << ""; return out; } @@ -1044,32 +936,61 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess return filter; } -QMap MinecraftInstance::makeProfileVarMapping(std::shared_ptr profile) const +MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) { - QMap result; - - result["profile_name"] = name(); - result["version_name"] = profile->getMinecraftVersion(); - result["version_type"] = profile->getMinecraftVersionType(); - - QString absRootDir = QDir(gameRoot()).absolutePath(); - result["game_directory"] = absRootDir; - QString absAssetsDir = QDir("assets/").absolutePath(); - auto assets = profile->getMinecraftAssets(); - result["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); - - // 1.7.3+ assets tokens - result["assets_root"] = absAssetsDir; - result["assets_index_name"] = assets->id; - - result["library_directory"] = APPLICATION->metacache()->getBasePath("libraries"); - - return result; + QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = re.match(line); + if (match.hasMatch()) { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + if (levelStr == "INFO") + level = MessageLevel::Message; + if (levelStr == "WARN") + level = MessageLevel::Warning; + if (levelStr == "ERROR") + level = MessageLevel::Error; + if (levelStr == "FATAL") + level = MessageLevel::Fatal; + if (levelStr == "TRACE" || levelStr == "DEBUG") + level = MessageLevel::Debug; + } else { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || + line.contains("[FINEST]")) + level = MessageLevel::Message; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * + static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; + if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at " + javaSymbol)) || + line.contains(QRegularExpression("Caused by: " + javaSymbol)) || + line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) || + line.contains(QRegularExpression("... \\d+ more$"))) + return MessageLevel::Error; + return level; } -QStringList MinecraftInstance::getLogFileSearchPaths() +IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() { - return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; + auto combined = std::make_shared(); + combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); + combined->add(std::make_shared("crash-.*\\.txt")); + combined->add(std::make_shared("IDMap dump.*\\.txt$")); + combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); + return combined; +} + +QString MinecraftInstance::getLogFileRoot() +{ + return gameRoot(); } QString MinecraftInstance::getStatusbarDescription() @@ -1117,23 +1038,24 @@ QList MinecraftInstance::createUpdateTask() // libraries download makeShared(this), // FML libraries download and copy into the instance - makeShared(this), + makeShared(this), // assets update makeShared(this), }; } -LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); - auto process = LaunchTask::create(this); + // FIXME: get rid of shared_from_this ... + auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_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", MessageLevel::Launcher)); + process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) @@ -1161,20 +1083,6 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf process->appendStep(step); } - // load meta - { - auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; - process->appendStep(makeShared(pptr, makeShared(this, mode))); - } - - // check java - { - 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 if (getPreLaunchCommand().size()) { auto step = makeShared(pptr); @@ -1182,14 +1090,26 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf process->appendStep(step); } - // if we aren't in offline mode - if (session->launchMode != LaunchMode::Offline) { - process->appendStep(makeShared(pptr, session)); + // load meta + { + auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStep(makeShared(pptr, makeShared(this, mode))); + } + + // check java + { + process->appendStep(makeShared(pptr)); + process->appendStep(makeShared(pptr)); + } + + // if we aren't in offline mode,. + if (session->status != AuthSession::PlayableOffline) { + if (!session->demo) { + process->appendStep(makeShared(pptr, session)); + } for (auto t : createUpdateTask()) { process->appendStep(makeShared(pptr, t)); } - } else { - process->appendStep(makeShared(pptr, this)); } // if there are any jar mods @@ -1202,11 +1122,6 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf 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)); @@ -1222,6 +1137,11 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf process->appendStep(makeShared(pptr)); } + // verify that minimum Java requirements are met + { + process->appendStep(makeShared(pptr)); + } + { // actually launch the game auto step = makeShared(pptr); @@ -1243,9 +1163,9 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf if (m_settings->get("QuitAfterGameStop").toBool()) { process->appendStep(makeShared(pptr)); } - m_launchProcess = std::move(process); - emit launchTaskChanged(m_launchProcess.get()); - return m_launchProcess.get(); + m_launchProcess = process; + emit launchTaskChanged(m_launchProcess); + return m_launchProcess; } JavaVersion MinecraftInstance::getJavaVersion() @@ -1253,80 +1173,71 @@ JavaVersion MinecraftInstance::getJavaVersion() return JavaVersion(settings()->get("JavaVersion").toString()); } -ModFolderModel* MinecraftInstance::loaderModList() +std::shared_ptr MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); } - return m_loader_mod_list.get(); + return m_loader_mod_list; } -ModFolderModel* MinecraftInstance::coreModList() +std::shared_ptr MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); } - return m_core_mod_list.get(); + return m_core_mod_list; } -ModFolderModel* MinecraftInstance::nilModList() +std::shared_ptr 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.get(); + return m_nil_mod_list; } -ResourcePackFolderModel* MinecraftInstance::resourcePackList() +std::shared_ptr 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)); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this)); } - return m_resource_pack_list.get(); + return m_resource_pack_list; } -TexturePackFolderModel* MinecraftInstance::texturePackList() +std::shared_ptr 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)); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this)); } - return m_texture_pack_list.get(); + return m_texture_pack_list; } -ShaderPackFolderModel* MinecraftInstance::shaderPackList() +std::shared_ptr 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)); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this)); } - return m_shader_pack_list.get(); + return m_shader_pack_list; } -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.get(); -} - -QList MinecraftInstance::resourceLists() -{ - return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; -} - -WorldList* MinecraftInstance::worldList() +std::shared_ptr MinecraftInstance::worldList() { if (!m_world_list) { m_world_list.reset(new WorldList(worldDir(), this)); } - return m_world_list.get(); + return m_world_list; +} + +std::shared_ptr MinecraftInstance::gameOptionsModel() +{ + if (!m_game_options) { + m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); + } + return m_game_options; } QList MinecraftInstance::getJarMods() const diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 909962d5e..75e97ae45 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -36,7 +36,6 @@ #pragma once #include -#include #include #include #include "BaseInstance.h" @@ -49,15 +48,15 @@ class ResourcePackFolderModel; class ShaderPackFolderModel; class TexturePackFolderModel; class WorldList; +class GameOptions; class LaunchStep; -class LaunchProfile; class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: - MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); - virtual ~MinecraftInstance(); + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + virtual ~MinecraftInstance() = default; virtual void saveNow() override; void loadSpecificSettings() override; @@ -81,7 +80,6 @@ class MinecraftInstance : public BaseInstance { QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; - QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; @@ -104,27 +102,26 @@ class MinecraftInstance : public BaseInstance { QString getLocalLibraryPath() const; /** Returns whether the instance, with its version, has support for demo mode. */ - bool supportsDemo() const; + [[nodiscard]] bool supportsDemo() const; void updateRuntimeContext() override; ////// Profile management ////// - PackProfile* getPackProfile() const; + std::shared_ptr getPackProfile() const; ////// Mod Lists ////// - ModFolderModel* loaderModList(); - ModFolderModel* coreModList(); - ModFolderModel* nilModList(); - ResourcePackFolderModel* resourcePackList(); - TexturePackFolderModel* texturePackList(); - ShaderPackFolderModel* shaderPackList(); - DataPackFolderModel* dataPackList(); - QList resourceLists(); - WorldList* worldList(); + 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 worldList(); + std::shared_ptr gameOptionsModel(); ////// Launch stuff ////// QList createUpdateTask() override; - LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; + shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; @@ -141,7 +138,12 @@ class MinecraftInstance : public BaseInstance { QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; - QStringList getLogFileSearchPaths() override; + /// guess log level from a line of minecraft log + MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override; + + IPathMatcher::Ptr getLogFileMatcher() override; + + QString getLogFileRoot() override; QString getStatusbarDescription() override; @@ -159,16 +161,17 @@ class MinecraftInstance : public BaseInstance { protected: QMap createCensorFilterFromSession(AuthSessionPtr session); - QMap makeProfileVarMapping(std::shared_ptr profile) const; protected: // data - 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; + 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_world_list; + mutable std::shared_ptr m_game_options; }; + +using MinecraftInstancePtr = std::shared_ptr; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index c26fb8b60..b9fb7eb0c 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -8,10 +8,7 @@ void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task auto components = m_inst->getPackProfile(); - if (auto result = components->reload(m_netmode); !result) { - emitFailed(result.error); - return; - } + components->reload(m_netmode); m_task = components->getCurrentTask(); if (!m_task) { @@ -20,8 +17,11 @@ 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, &MinecraftLoadAndCheck::emitAborted); - propagateFromOther(m_task.get()); + 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); } bool MinecraftLoadAndCheck::canAbort() const @@ -35,7 +35,9 @@ bool MinecraftLoadAndCheck::canAbort() const bool MinecraftLoadAndCheck::abort() { if (m_task && m_task->canAbort()) { - return m_task->abort(); + auto status = m_task->abort(); + emitFailed("Aborted."); + return status; } return Task::abort(); -} +} \ No newline at end of file diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index 42730b12d..d17a3a21f 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -319,11 +319,7 @@ LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, cons } if (libObj.contains("rules")) { out->applyRules = true; - - QJsonArray rulesArray = requireArray(libObj.value("rules")); - for (auto rule : rulesArray) { - out->m_rules.append(Rule::fromJson(requireObject(rule))); - } + out->m_rules = rulesFromJsonV4(libObj); } if (libObj.contains("downloads")) { out->m_mojangDownloads = libDownloadInfoFromJson(libObj); @@ -359,7 +355,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library* library) if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto& rule : library->m_rules) { - QJsonObject ruleObj = rule.toJson(); + QJsonObject ruleObj = rule->toJson(); allRules.append(ruleObj); } libRoot.insert("rules", allRules); diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 95f4c5ef6..bd587beb2 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->uid = root.value("fileId").toString(); } - static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( + const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; - if (!s_validUidRegex.match(out->uid).hasMatch()) { + if (!valid_uid_regex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); @@ -176,7 +176,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc } } - auto readLibs = [&root, &out, &filename](const char* which, QList& outList) { + auto readLibs = [&](const char* which, QList& outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library @@ -209,7 +209,8 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc QString arg = ""; readString(agentObj, "argument", arg); - out->agents.append(Agent{ lib, arg }); + AgentPtr agent(new Agent(lib, arg)); + out->agents.append(agent); } } @@ -258,8 +259,8 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc if (root.contains("runtimes")) { out->runtimes = {}; - for (auto runtime : root["runtimes"].toArray()) { - out->runtimes.append(Java::parseJavaMeta(runtime.toObject())); + for (auto runtime : ensureArray(root, "runtimes")) { + out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime))); } } @@ -304,10 +305,10 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch writeStringList(root, "+jvmArgs", patch->addnJvmArguments); if (!patch->agents.isEmpty()) { QJsonArray array; - for (const auto& value : patch->agents) { - QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value.library.get()); - if (!value.argument.isEmpty()) - agentOut.insert("argument", value.argument); + for (auto value : patch->agents) { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); + if (!value->argument().isEmpty()) + agentOut.insert("argument", value->argument()); array.append(agentOut); } @@ -369,7 +370,8 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemConta } // just make up something unique on the spot for the library name. - QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto uuid = QUuid::createUuid(); + QString id = uuid.toString().remove('{').remove('}'); 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 f0cff7f0e..f1d2473c2 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -38,6 +38,7 @@ */ #include +#include #include #include #include @@ -129,18 +130,18 @@ static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& comp auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); auto component = makeShared(parent, uid); - component->m_version = obj.value("version").toString(); - component->m_dependencyOnly = obj.value("dependencyOnly").toBool(); - component->m_important = obj.value("important").toBool(); + component->m_version = Json::ensureString(obj.value("version")); + component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); + component->m_important = Json::ensureBoolean(obj.value("important"), false); // cached // TODO @RESILIENCE: ignore invalid values/structure here? - component->m_cachedVersion = obj.value("cachedVersion").toString(); - component->m_cachedName = obj.value("cachedName").toString(); + component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); + component->m_cachedName = Json::ensureString(obj.value("cachedName")); Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); - component->m_cachedVolatile = obj.value("volatile").toBool(); - bool disabled = obj.value("disabled").toBool(); + component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); + bool disabled = Json::ensureBoolean(obj.value("disabled"), false); component->setEnabled(!disabled); return component; } @@ -167,38 +168,34 @@ 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; } // Read the given file into component containers -static PackProfile::Result loadPackProfile(PackProfile* parent, - const QString& filename, - const QString& componentJsonPattern, - ComponentContainer& container) +static bool loadPackProfile(PackProfile* parent, + const QString& filename, + const QString& componentJsonPattern, + ComponentContainer& container) { QFile componentsFile(filename); if (!componentsFile.exists()) { - auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename); - qCWarning(instanceProfileC) << message; - return PackProfile::Result::Error(message); + qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen."; + return false; } if (!componentsFile.open(QFile::ReadOnly)) { - auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString()); - qCCritical(instanceProfileC) << message; + qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString(); qCWarning(instanceProfileC) << "Ignoring overridden order"; - return PackProfile::Result::Error(message); + return false; } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { - auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString()); - qCCritical(instanceProfileC) << message; + qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); qCWarning(instanceProfileC) << "Ignoring overridden order"; - return PackProfile::Result::Error(message); + return false; } // and then read it and process it if all above is true. @@ -215,13 +212,11 @@ static PackProfile::Result loadPackProfile(PackProfile* parent, container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] const JSONValidationError& err) { - auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName()); - qCCritical(instanceProfileC) << message; - qCWarning(instanceProfileC) << "error:" << err.what(); + qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; container.clear(); - return PackProfile::Result::Error(message); + return false; } - return PackProfile::Result::Success(); + return true; } // END: component file format @@ -230,8 +225,9 @@ static PackProfile::Result loadPackProfile(PackProfile* parent, void PackProfile::saveNow() { - if (saveIsScheduled() && save_internal()) { + if (saveIsScheduled()) { d->m_saveTimer.stop(); + save_internal(); } } @@ -279,62 +275,52 @@ QString PackProfile::patchFilePathForUid(const QString& uid) const return patchesPattern().arg(uid); } -bool PackProfile::save_internal() +void PackProfile::save_internal() { qDebug() << d->m_instance->name() << "|" << "Component list save performed now"; auto filename = componentsFilePath(); - if (savePackProfile(filename, d->components)) { - d->dirty = false; - return true; - } - return false; + savePackProfile(filename, d->components); + d->dirty = false; } -PackProfile::Result PackProfile::load() +bool PackProfile::load() { auto filename = componentsFilePath(); // load the new component list and swap it with the current one... ComponentContainer newComponents; - if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) { + if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) { qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; - return result; - } - // FIXME: actually use fine-grained updates, not this... - beginResetModel(); - // disconnect all the old components - for (auto component : d->components) { - disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); - } - d->components.clear(); - d->componentIndex.clear(); - for (auto component : newComponents) { - if (d->componentIndex.contains(component->m_uid)) { - qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid; - continue; + return false; + } else { + // FIXME: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for (auto component : d->components) { + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); } - connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); - d->components.append(component); - d->componentIndex[component->m_uid] = component; + d->components.clear(); + d->componentIndex.clear(); + for (auto component : newComponents) { + if (d->componentIndex.contains(component->m_uid)) { + qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return true; } - endResetModel(); - d->loaded = true; - return Result::Success(); } -PackProfile::Result PackProfile::reload(Net::Mode netmode) +void PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if (d->m_updateTask) { - 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(); + return; } // flush any scheduled saves to not lose state @@ -343,11 +329,9 @@ PackProfile::Result PackProfile::reload(Net::Mode netmode) // FIXME: differentiate when a reapply is required by propagating state from components invalidateLaunchProfile(); - if (auto result = load(); !result) { - return result; + if (load()) { + resolve(netmode); } - resolve(netmode); - return Result::Success(); } Task::Ptr PackProfile::getCurrentTask() @@ -374,7 +358,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(); } @@ -527,9 +511,13 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const switch (role) { case Qt::CheckStateRole: { - if (column == NameColumn) - return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; - return QVariant(); + switch (column) { + case NameColumn: { + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + } + default: + return QVariant(); + } } case Qt::DisplayRole: { switch (column) { @@ -655,7 +643,11 @@ void PackProfile::move(const int index, const MoveDirection direction) return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); +#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) d->components.swapItemsAt(index, theirIndex); +#else + d->components.swap(index, theirIndex); +#endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); @@ -754,7 +746,7 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch) } // FIXME: we need a generic way of removing local resources, not just jar mods... - auto preRemoveJarMod = [this](LibraryPtr jarMod) -> bool { + auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool { if (!jarMod->isLocal()) { return true; } @@ -925,7 +917,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths) agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); - versionFile->agents.append(Agent{agent, QString()}); + versionFile->agents.append(std::make_shared(agent, QString())); versionFile->name = targetName; versionFile->uid = targetId; @@ -962,7 +954,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 70dd04514..b2de26ea0 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -62,19 +62,6 @@ class PackProfile : public QAbstractListModel { public: enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS }; - struct Result { - bool success; - QString error; - - // Implicit conversion to bool - operator bool() const { return success; } - - // Factory methods for convenience - static Result Success() { return { true, "" }; } - - static Result Error(const QString& errorMessage) { return { false, errorMessage }; } - }; - explicit PackProfile(MinecraftInstance* instance); virtual ~PackProfile(); @@ -115,7 +102,7 @@ class PackProfile : public QAbstractListModel { bool revertToBase(int index); /// reload the list, reload all components, resolve dependencies - Result reload(Net::Mode netmode); + void reload(Net::Mode netmode); // reload all components, resolve dependencies void resolve(Net::Mode netmode); @@ -175,14 +162,14 @@ class PackProfile : public QAbstractListModel { QString patchesPattern() const; private slots: - bool save_internal(); + void save_internal(); void updateSucceeded(); void updateFailed(const QString& error); void componentDataChanged(); void disableInteraction(bool disable); private: - Result load(); + bool load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); bool installAgents_internal(QStringList filepaths); diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index feb825904..4fb3621f0 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; - shared_qobject_ptr m_updateTask; + Task::Ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index ae6326953..08ec0fac3 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -41,6 +41,7 @@ #include #include +#include #include namespace ProfileUtils { @@ -55,7 +56,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 +145,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 << "error:" << jsonFile.errorString(); + qWarning() << "Couldn't save" << filename; return false; } return true; diff --git a/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp index 606776e8a..d80aab84d 100644 --- a/launcher/minecraft/Rule.cpp +++ b/launcher/minecraft/Rule.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * 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 @@ -39,54 +38,72 @@ #include "Rule.h" -Rule Rule::fromJson(const QJsonObject& object) +RuleAction RuleAction_fromString(QString name) { - Rule result; + if (name == "allow") + return Allow; + if (name == "disallow") + return Disallow; + return Defer; +} - if (object["action"] == "allow") - result.m_action = Allow; - else if (object["action"] == "disallow") - result.m_action = Disallow; +QList> rulesFromJsonV4(const QJsonObject& objectWithRules) +{ + QList> rules; + auto rulesVal = objectWithRules.value("rules"); + if (!rulesVal.isArray()) + return rules; - if (auto os = object["os"]; os.isObject()) { - if (auto name = os["name"].toString(); !name.isNull()) { - result.m_os = OS{ - name, - os["version"].toString(), - }; + QJsonArray ruleList = rulesVal.toArray(); + for (auto ruleVal : ruleList) { + std::shared_ptr rule; + if (!ruleVal.isObject()) + continue; + auto ruleObj = ruleVal.toObject(); + auto actionVal = ruleObj.value("action"); + if (!actionVal.isString()) + continue; + auto action = RuleAction_fromString(actionVal.toString()); + if (action == Defer) + continue; + + auto osVal = ruleObj.value("os"); + if (!osVal.isObject()) { + // add a new implicit action rule + rules.append(ImplicitRule::create(action)); + continue; + } + + auto osObj = osVal.toObject(); + auto osNameVal = osObj.value("name"); + if (!osNameVal.isString()) + continue; + QString osName = osNameVal.toString(); + QString versionRegex = osObj.value("version").toString(); + // add a new OS rule + rules.append(OsRule::create(action, osName, versionRegex)); + } + return rules; +} + +QJsonObject ImplicitRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + return ruleObj; +} + +QJsonObject OsRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + QJsonObject osObj; + { + osObj.insert("name", m_system); + if (!m_version_regexp.isEmpty()) { + osObj.insert("version", m_version_regexp); } } - - return result; -} - -QJsonObject Rule::toJson() -{ - QJsonObject result; - - if (m_action == Allow) - result["action"] = "allow"; - else if (m_action == Disallow) - result["action"] = "disallow"; - - if (m_os.has_value()) { - QJsonObject os; - - os["name"] = m_os->name; - - if (!m_os->version.isEmpty()) - os["version"] = m_os->version; - - result["os"] = os; - } - - return result; -} - -Rule::Action Rule::apply(const RuntimeContext& runtimeContext) -{ - if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) - return Defer; - - return m_action; + ruleObj.insert("os", osObj); + return ruleObj; } diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index b0b689fd7..c6cdbc43f 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * 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 @@ -39,27 +38,59 @@ #include #include #include +#include #include "RuntimeContext.h" class Library; +class Rule; + +enum RuleAction { Allow, Disallow, Defer }; + +QList> rulesFromJsonV4(const QJsonObject& objectWithRules); class Rule { + protected: + RuleAction m_result; + virtual bool applies(const Library* parent, const RuntimeContext& runtimeContext) = 0; + public: - enum Action { Allow, Disallow, Defer }; - - static Rule fromJson(const QJsonObject& json); - QJsonObject toJson(); - - Action apply(const RuntimeContext& runtimeContext); - - private: - struct OS { - QString name; - // FIXME: unsupported - // retained to avoid information being lost from files - QString version; - }; - - Action m_action = Defer; - std::optional m_os; + Rule(RuleAction result) : m_result(result) {} + virtual ~Rule() {} + virtual QJsonObject toJson() = 0; + RuleAction apply(const Library* parent, const RuntimeContext& runtimeContext) + { + if (applies(parent, runtimeContext)) + return m_result; + else + return Defer; + } +}; + +class OsRule : public Rule { + private: + // the OS + QString m_system; + // the OS version regexp + QString m_version_regexp; + + protected: + virtual bool applies(const Library*, const RuntimeContext& runtimeContext) { return runtimeContext.classifierMatches(m_system); } + OsRule(RuleAction result, QString system, QString version_regexp) : Rule(result), m_system(system), m_version_regexp(version_regexp) {} + + public: + virtual QJsonObject toJson(); + static std::shared_ptr create(RuleAction result, QString system, QString version_regexp) + { + return std::shared_ptr(new OsRule(result, system, version_regexp)); + } +}; + +class ImplicitRule : public Rule { + protected: + virtual bool applies(const Library*, [[maybe_unused]] const RuntimeContext& runtimeContext) { return true; } + ImplicitRule(RuleAction result) : Rule(result) {} + + public: + virtual QJsonObject toJson(); + static std::shared_ptr create(RuleAction result) { return std::shared_ptr(new ImplicitRule(result)); } }; diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp deleted file mode 100644 index b719e3142..000000000 --- a/launcher/minecraft/ShortcutUtils.cpp +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2023 TheKodeToad - * Copyright (C) 2025 Yihe Li - * - * parent 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. - * - * parent 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 parent program. If not, see . - * - * parent 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 parent 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 "ShortcutUtils.h" - -#include "FileSystem.h" - -#include -#include - -#include -#include -#include - -namespace ShortcutUtils { - -bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) -{ - if (!shortcut.instance) - return false; - - QString appPath = QApplication::applicationFilePath(); - auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - QString iconPath; - QStringList args; -#if defined(Q_OS_MACOS) - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return false; - } - - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application: %1").arg(iconFile.errorString())); - return false; - } - - QIcon iconObj = icon->icon(); - bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); - return false; - } -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) { - QMessageBox::critical( - shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } else if (appPath.endsWith("/")) { - appPath.chop(1); - } - } - - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - 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"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return false; - } - - if (DesktopServices::isFlatpak()) { - appPath = "flatpak"; - args.append({ "run", BuildConfig.LAUNCHER_APPID }); - } - -#elif defined(Q_OS_WIN) - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); - - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but parent 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->logo(); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - 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"); - iconFile.close(); - - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return false; - } - -#else - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); - return false; -#endif - args.append({ "--launch", shortcut.instance->id() }); - args.append(shortcut.extraArgs); - - QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); - if (shortcutPath.isEmpty()) { -#if not defined(Q_OS_MACOS) - iconFile.remove(); -#endif - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); - return false; - } - - shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); - return true; -} - -bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) -{ - if (!shortcut.instance) - return false; - - QString desktopDir = FS::getDesktopDir(); - if (desktopDir.isEmpty()) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); - return false; - } - - QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); - if (!createInstanceShortcut(shortcut, shortcutFilePath)) - return false; - QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); - return true; -} - -bool createInstanceShortcutInApplications(const Shortcut& shortcut) -{ - if (!shortcut.instance) - return false; - - QString applicationsDir = FS::getApplicationsDir(); - if (applicationsDir.isEmpty()) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); - return false; - } - -#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) - applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); - - QDir applicationsDirQ(applicationsDir); - if (!applicationsDirQ.mkpath(".")) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Failed to create instances folder in applications folder!")); - return false; - } -#endif - - QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); - if (!createInstanceShortcut(shortcut, shortcutFilePath)) - return false; - QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); - return true; -} - -bool createInstanceShortcutInOther(const Shortcut& shortcut) -{ - if (!shortcut.instance) - return false; - - QString defaultedDir = FS::getDesktopDir(); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - QString extension = ".desktop"; -#elif defined(Q_OS_WINDOWS) - QString extension = ".lnk"; -#else - QString extension = ""; -#endif - - QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(defaultedDir); - - shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, - QObject::tr("Desktop Entries") + " (*" + extension + ")"); - if (shortcutFilePath.isEmpty()) - return false; // file dialog canceled by user - - if (shortcutFilePath.endsWith(extension)) - shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); - if (!createInstanceShortcut(shortcut, shortcutFilePath)) - return false; - QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), - QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); - return true; -} - -} // namespace ShortcutUtils diff --git a/launcher/minecraft/VanillaInstanceCreationTask.cpp b/launcher/minecraft/VanillaInstanceCreationTask.cpp index e646e2c52..ccbd8c677 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.cpp +++ b/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -7,27 +7,32 @@ #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" -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)) +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)) {} -std::unique_ptr VanillaCreationTask::createInstance() +bool VanillaCreationTask::createInstance() { setStatus(tr("Creating instance from version %1").arg(m_version->name())); - auto inst = std::make_unique( - m_globalSettings, std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), m_stagingPath); - SettingsObject::Lock lock(inst->settings()); + 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* 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()); + inst.setName(name()); + inst.setIconKey(m_instIcon); } + instance_settings->resumeSave(); - inst->setName(name()); - inst->setIconKey(m_instIcon); - components->saveNow(); - return inst; + return true; } diff --git a/launcher/minecraft/VanillaInstanceCreationTask.h b/launcher/minecraft/VanillaInstanceCreationTask.h index c1a69ab62..d1b816824 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.h +++ b/launcher/minecraft/VanillaInstanceCreationTask.h @@ -7,10 +7,10 @@ class VanillaCreationTask final : public InstanceCreationTask { Q_OBJECT public: - explicit VanillaCreationTask(BaseVersion::Ptr version) : m_version(std::move(version)) {} - VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion); + VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} + VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); - std::unique_ptr createInstance() override; + bool createInstance() override; private: // Version to update to / create of the instance. diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 32a7504ac..40f49aaa4 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 0deecb042..bd28f9e9a 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -43,6 +43,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -54,7 +57,6 @@ #include "FileSystem.h" #include "PSaveFile.h" -#include "archive/ArchiveReader.h" using std::nullopt; using std::optional; @@ -153,23 +155,18 @@ QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) return val; } -QString getDatFromFS(const QFileInfo& root, QString file) -{ - QDir worldDir(root.filePath()); - if (!root.isDir() || !worldDir.exists(file)) { - return QString(); - } - return worldDir.absoluteFilePath(file); -} - QString getLevelDatFromFS(const QFileInfo& file) { - return getDatFromFS(file, "level.dat"); + QDir worldDir(file.filePath()); + if (!file.isDir() || !worldDir.exists("level.dat")) { + return QString(); + } + return worldDir.absoluteFilePath("level.dat"); } -QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +QByteArray getLevelDatDataFromFS(const QFileInfo& file) { - auto fullFilePath = getDatFromFS(root, file); + auto fullFilePath = getLevelDatFromFS(file); if (fullFilePath.isNull()) { return QByteArray(); } @@ -180,16 +177,6 @@ QByteArray getDatDataFromFS(const QFileInfo& root, QString 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); @@ -211,6 +198,22 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) return f.commit(); } +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") { + return file.size(); + } else if (file.isDir()) { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + World::World(const QFileInfo& file) { repath(file); @@ -220,6 +223,7 @@ void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); + m_size = calculateWorldSize(file); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); @@ -244,43 +248,49 @@ bool World::resetIcon() return false; } -int64_t loadSeed(QByteArray data); - void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { - m_isValid = false; + is_valid = false; return; } loadFromLevelDat(bytes); - m_levelDatTime = file.lastModified(); - if (m_randomSeed == 0) { - bytes = getWorldGenDataFromFS(file); - if (!bytes.isEmpty()) { - m_randomSeed = loadSeed(bytes); - } - } + levelDatTime = file.lastModified(); } void World::readFromZip(const QFileInfo& file) { - MMCZip::ArchiveReader r(file.absoluteFilePath()); - - m_isValid = false; - r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) { - const QString levelDat = "level.dat"; - auto filePath = file->filename(); - QFileInfo fi(filePath); - if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { - m_containerOffsetPath = filePath.chopped(levelDat.length()); - m_levelDatTime = file->dateTime(); - loadFromLevelDat(file->readAll()); - m_isValid = true; - stop = true; - } - return true; - }); + QuaZip zip(file.absoluteFilePath()); + is_valid = zip.open(QuaZip::mdUnzip); + if (!is_valid) { + return; + } + auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + is_valid = !location.isEmpty(); + if (!is_valid) { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + is_valid = zip.setCurrentFile(location + "level.dat"); + if (!is_valid) { + return; + } + is_valid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if (!modTime.isValid()) { + modTime = levelDatInfo.dateTime; + } + levelDatTime = modTime; + if (!is_valid) { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); } bool World::install(const QString& to, const QString& name) @@ -291,7 +301,10 @@ bool World::install(const QString& to, const QString& name) } bool ok = false; if (m_containerFile.isFile()) { - MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath()); + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) { + return false; + } ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if (m_containerFile.isDir()) { QString from = m_containerFile.filePath(); @@ -354,7 +367,7 @@ optional read_string(nbt::value& parent, const char* name) return nullopt; } auto& tag_str = namedValue.as(); - return QString::fromUtf8(tag_str.get()); + return QString::fromStdString(tag_str.get()); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; @@ -413,33 +426,11 @@ 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); if (!levelData) { - m_isValid = false; + is_valid = false; return; } @@ -447,21 +438,21 @@ void World::loadFromLevelDat(QByteArray data) try { valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { - qWarning().nospace() << "Unable to read NBT tags from " << m_folderName << ": " << e.what(); - m_isValid = false; + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + is_valid = false; return; } nbt::value& val = *valPtr; - m_isValid = val.get_type() == nbt::tag_type::Compound; - if (!m_isValid) + is_valid = val.get_type() == nbt::tag_type::Compound; + if (!is_valid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); - m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; m_gameType = read_gametype(val, "GameType"); @@ -499,7 +490,7 @@ bool World::replace(World& with) bool World::destroy() { - if (!m_isValid) + if (!is_valid) return false; if (FS::trash(m_containerFile.filePath())) @@ -517,7 +508,7 @@ bool World::destroy() bool World::operator==(const World& other) const { - return m_isValid == other.m_isValid && folderName() == other.folderName(); + return is_valid == other.is_valid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const @@ -540,8 +531,3 @@ bool World::isMoreThanOneHardLink() const } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } - -void World::setSize(int64_t size) -{ - m_size = size; -} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index bb4baa33d..4303dc553 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -39,7 +39,7 @@ class World { QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } - bool isValid() const { return m_isValid; } + bool isValid() const { return is_valid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world @@ -54,12 +54,10 @@ class World { bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); - void setSize(int64_t size); - // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; - auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } + [[nodiscard]] auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance @@ -68,9 +66,9 @@ class World { * @return true * @return false */ - bool isSymLinkUnder(const QString& instPath) const; + [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; - bool isMoreThanOneHardLink() const; + [[nodiscard]] bool isMoreThanOneHardLink() const; QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } @@ -85,10 +83,10 @@ class World { QString m_folderName; QString m_actualName; QString m_iconFile; - QDateTime m_levelDatTime; + QDateTime levelDatTime; QDateTime m_lastPlayed; - int64_t m_size = 0; + int64_t m_size; int64_t m_randomSeed = 0; GameType m_gameType; - bool m_isValid = false; + bool is_valid = false; }; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index ca8ac1aa8..812b13c71 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -37,14 +37,13 @@ #include #include -#include #include #include #include -#include #include #include #include +#include "Application.h" WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { @@ -52,34 +51,34 @@ WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractList m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); - m_isWatching = false; + is_watching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { - if (m_isWatching) { + if (is_watching) { return; } update(); - m_isWatching = m_watcher->addPath(m_dir.absolutePath()); - if (m_isWatching) { - qDebug() << "Started watching" << m_dir.absolutePath(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << m_dir.absolutePath(); } else { - qDebug() << "Failed to start watching" << m_dir.absolutePath(); + qDebug() << "Failed to start watching " << m_dir.absolutePath(); } } void WorldList::stopWatching() { - if (!m_isWatching) { + if (!is_watching) { return; } - m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); - if (!m_isWatching) { - qDebug() << "Stopped watching" << m_dir.absolutePath(); + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { - qDebug() << "Failed to stop watching" << m_dir.absolutePath(); + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); } } @@ -102,13 +101,12 @@ bool WorldList::update() } } beginResetModel(); - m_worlds.swap(newWorlds); + worlds.swap(newWorlds); endResetModel(); - loadWorldsAsync(); return true; } -void WorldList::directoryChanged(QString) +void WorldList::directoryChanged(QString path) { update(); } @@ -125,12 +123,12 @@ QString WorldList::instDirPath() const bool WorldList::deleteWorld(int index) { - if (index >= m_worlds.size() || index < 0) + if (index >= worlds.size() || index < 0) return false; - World& m = m_worlds[index]; + World& m = worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); - m_worlds.removeAt(index); + worlds.removeAt(index); endRemoveRows(); emit changed(); return true; @@ -141,11 +139,11 @@ bool WorldList::deleteWorld(int index) bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { - World& m = m_worlds[i]; + World& m = worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); - m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); + worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; @@ -153,12 +151,11 @@ bool WorldList::deleteWorlds(int first, int last) bool WorldList::resetIcon(int row) { - if (row >= m_worlds.size() || row < 0) + if (row >= worlds.size() || row < 0) return false; - World& m = m_worlds[row]; + World& m = worlds[row]; if (m.resetIcon()) { - QModelIndex modelIndex = index(row, NameColumn); - emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole }); + emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); return true; } return false; @@ -177,12 +174,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= m_worlds.size()) + if (row < 0 || row >= worlds.size()) return QVariant(); QLocale locale; - auto& world = m_worlds[row]; + auto& world = worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { @@ -211,9 +208,13 @@ QVariant WorldList::data(const QModelIndex& index, int role) const } case Qt::UserRole: - if (column == SizeColumn) - return QVariant::fromValue(world.bytes()); - return data(index, Qt::DisplayRole); + switch (column) { + case SizeColumn: + return QVariant::fromValue(world.bytes()); + + default: + return data(index, Qt::DisplayRole); + } case Qt::ToolTipRole: { if (column == InfoColumn) { @@ -302,31 +303,54 @@ QStringList WorldList::mimeTypes() const return types; } +class WorldMimeData : public QMimeData { + Q_OBJECT + + public: + WorldMimeData(QList worlds) { m_worlds = worlds; } + QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } + + protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QVariant retrieveData(const QString& mimetype, QMetaType type) const +#else + QVariant retrieveData(const QString& mimetype, QVariant::Type type) const +#endif + { + QList urls; + for (auto& world : m_worlds) { + if (!world.isValid() || !world.isOnFS()) + continue; + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + const_cast(this)->setUrls(urls); + return QMimeData::retrieveData(mimetype, type); + } + + private: + QList m_worlds; +}; + QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const { - QList urls; + if (indexes.size() == 0) + return new QMimeData(); + QList worlds_; for (auto idx : indexes) { if (idx.column() != 0) continue; - int row = idx.row(); - if (row < 0 || row >= this->m_worlds.size()) + if (row < 0 || row >= this->worlds.size()) continue; - - const World& world = m_worlds[row]; - - if (!world.isValid() || !world.isOnFS()) - continue; - - QString worldPath = world.container().absoluteFilePath(); - qDebug() << worldPath; - urls.append(QUrl::fromLocalFile(worldPath)); + worlds_.append(this->worlds[row]); } - - auto result = new QMimeData(); - result->setUrls(urls); - return result; + if (!worlds_.size()) { + return new QMimeData(); + } + return new WorldMimeData(worlds_); } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const @@ -352,7 +376,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; @@ -373,7 +397,7 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; // files dropped from outside? if (data->hasUrls()) { - bool was_watching = m_isWatching; + bool was_watching = is_watching; if (was_watching) stopWatching(); auto urls = data->urls(); @@ -396,42 +420,4 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; } -int64_t calculateWorldSize(const QFileInfo& file) -{ - if (file.isFile() && file.suffix() == "zip") { - return file.size(); - } else if (file.isDir()) { - QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); - int64_t total = 0; - while (it.hasNext()) { - it.next(); - total += it.fileInfo().size(); - } - return total; - } - return -1; -} - -void WorldList::loadWorldsAsync() -{ - for (int i = 0; i < m_worlds.size(); ++i) { - auto file = m_worlds.at(i).container(); - int row = i; - QThreadPool::globalInstance()->start([this, file, row]() mutable { - auto size = calculateWorldSize(file); - - QMetaObject::invokeMethod( - this, - [this, size, row, file]() { - if (row < m_worlds.size() && m_worlds[row].container() == file) { - m_worlds[row].setSize(size); - - // Notify views - QModelIndex modelIndex = index(row, SizeColumn); - emit dataChanged(modelIndex, modelIndex, { SizeRole }); - } - }, - Qt::QueuedConnection); - }); - } -} +#include "WorldList.moc" diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 93fecf1f5..bea24bb9a 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -40,9 +40,9 @@ class WorldList : public QAbstractListModel { virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; - size_t size() const { return m_worlds.size(); }; + size_t size() const { return worlds.size(); }; bool empty() const { return size() == 0; } - World& operator[](size_t index) { return m_worlds[index]; } + World& operator[](size_t index) { return worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); @@ -82,11 +82,10 @@ class WorldList : public QAbstractListModel { QString instDirPath() const; - const QList& allWorlds() const { return m_worlds; } + const QList& allWorlds() const { return worlds; } private slots: void directoryChanged(QString path); - void loadWorldsAsync(); signals: void changed(); @@ -94,7 +93,7 @@ class WorldList : public QAbstractListModel { protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; - bool m_isWatching; + bool is_watching; QDir m_dir; - QList m_worlds; + QList worlds; }; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index bfb350a63..fd2082035 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -38,10 +38,11 @@ #include #include #include +#include #include namespace { -void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) +void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName) { if (!t.persistent) { return; @@ -180,7 +181,6 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); - out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); out.skin.variant = variantV.toString(); // data for skin is optional @@ -217,7 +217,6 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); - cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); cape.alias = aliasV.toString(); // data for cape is optional. @@ -303,6 +302,7 @@ 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"); } @@ -332,6 +332,7 @@ 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"; @@ -356,10 +357,28 @@ QString AccountData::profileId() const QString AccountData::profileName() const { if (minecraftProfile.name.size() == 0) { - return QObject::tr("No Minecraft profile"); + return QObject::tr("No profile (%1)").arg(accountDisplayString()); + } else { + return minecraftProfile.name; } +} - return minecraftProfile.name; +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"; + } + } } QString AccountData::lastError() const diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 96ef94f30..1ada4e38a 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -36,12 +36,12 @@ #pragma once #include #include -#include #include +#include #include #include -#include +#include #include enum class Validity { None, Assumed, Certain }; @@ -96,6 +96,9 @@ 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; @@ -109,6 +112,7 @@ struct AccountData { QString msaClientID; Token msaToken; Token userToken; + Token xboxApiToken; Token mojangservicesToken; Token yggdrasilToken; @@ -119,6 +123,5 @@ 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 418ba98d2..d276d4c41 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -39,7 +39,6 @@ #include #include -#include #include #include #include @@ -169,26 +168,6 @@ 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; @@ -281,30 +260,6 @@ int AccountList::count() const return m_accounts.count(); } -QString getAccountStatus(AccountState status) -{ - switch (status) { - case AccountState::Unchecked: - return QObject::tr("Unchecked", "Account status"); - case AccountState::Offline: - return QObject::tr("Offline", "Account status"); - case AccountState::Online: - return QObject::tr("Ready", "Account status"); - case AccountState::Working: - return QObject::tr("Working", "Account status"); - case AccountState::Errored: - return QObject::tr("Errored", "Account status"); - case AccountState::Expired: - return QObject::tr("Expired", "Account status"); - case AccountState::Disabled: - return QObject::tr("Disabled", "Account status"); - case AccountState::Gone: - return QObject::tr("Gone", "Account status"); - default: - return QObject::tr("Unknown", "Account status"); - } -} - QVariant AccountList::data(const QModelIndex& index, int role) const { if (!index.isValid()) @@ -316,28 +271,15 @@ 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: + case ProfileNameColumn: { return account->profileName(); + } + + case NameColumn: + return account->accountDisplayString(); + case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { @@ -349,19 +291,55 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } return tr("Unknown", "Account type"); } - case StatusColumn: - return getAccountStatus(account->accountState()); + + case StatusColumn: { + switch (account->accountState()) { + case AccountState::Unchecked: { + return tr("Unchecked", "Account status"); + } + case AccountState::Offline: { + return tr("Offline", "Account status"); + } + case AccountState::Online: { + return tr("Ready", "Account status"); + } + case AccountState::Working: { + return tr("Working", "Account status"); + } + case AccountState::Errored: { + return tr("Errored", "Account status"); + } + case AccountState::Expired: { + return tr("Expired", "Account status"); + } + case AccountState::Disabled: { + return tr("Disabled", "Account status"); + } + case AccountState::Gone: { + return tr("Gone", "Account status"); + } + default: { + return tr("Unknown", "Account status"); + } + } + } + default: return QVariant(); } + case Qt::ToolTipRole: + return account->accountDisplayString(); + case PointerRole: return QVariant::fromValue(account); case Qt::CheckStateRole: - if (index.column() == ProfileNameColumn) + if (index.column() == ProfileNameColumn) { return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; - return QVariant(); + } else { + return QVariant(); + } default: return QVariant(); @@ -375,6 +353,8 @@ 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: @@ -387,6 +367,8 @@ 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: @@ -450,7 +432,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 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); return false; } @@ -479,14 +461,18 @@ bool AccountList::loadList() // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); - if (listVersion == AccountListVersion::MojangMSA) - return loadV3(root); - - QString newName = "accounts-old.json"; - qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; - // Attempt to rename the old version. - file.rename(newName); - return false; + switch (listVersion) { + case AccountListVersion::MojangMSA: { + return loadV3(root); + } break; + default: { + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; + } + } } bool AccountList::loadV3(QJsonObject& root) @@ -567,7 +553,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 save the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); return false; } @@ -578,7 +564,7 @@ bool AccountList::saveList() qDebug() << "Saved account list to" << m_listFilePath; return true; } else { - qDebug() << "Failed to save accounts to" << m_listFilePath << "error:" << file.errorString(); + qDebug() << "Failed to save accounts to" << m_listFilePath; return false; } } @@ -604,7 +590,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++) { @@ -628,7 +614,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(); } @@ -640,7 +626,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() @@ -648,32 +634,21 @@ 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->profileName() << "with internal ID" + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; return; } - break; } } - if (!found) { - qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; - } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); @@ -688,7 +663,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); } @@ -710,7 +685,7 @@ void AccountList::beginActivity() void AccountList::endActivity() { if (m_activityCount == 0) { - qWarning() << "Activity count would become below zero"; + qWarning() << m_name << " - Activity count would become below zero"; return; } bool deactivating = m_activityCount == 1; diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 916f23341..d3be6740e 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -55,6 +55,7 @@ class AccountList : public QAbstractListModel { enum VListColumns { // TODO: Add icon column. ProfileNameColumn = 0, + NameColumn, TypeColumn, StatusColumn, @@ -77,7 +78,6 @@ 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; @@ -111,6 +111,7 @@ class AccountList : public QAbstractListModel { void endActivity(); private: + const char* m_name; uint32_t m_activityCount = 0; signals: void listChanged(); diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 5b8f98122..19fbe15dd 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -10,6 +11,7 @@ #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" @@ -31,9 +33,11 @@ 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)); @@ -66,7 +70,6 @@ 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); @@ -90,9 +93,7 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason) return true; } case AccountTaskState::STATE_WORKING: { - if (!m_currentStep) { - setStatus(tr("Preparing to log in...")); - } + setStatus(m_currentStep ? m_currentStep->describe() : tr("Working...")); m_data->accountState = AccountState::Working; return true; } @@ -148,8 +149,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 d881a7691..bff4c04e4 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -2,8 +2,10 @@ #include #include +#include #include #include +#include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 85d77be9c..3657befec 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -20,17 +20,23 @@ 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; - launchMode = LaunchMode::Demo; -}; + status = PlayableOnline; // needs online to download the assets +}; \ No newline at end of file diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 07db54213..54e7d69e0 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -1,11 +1,12 @@ #pragma once +#include #include #include - -#include "LaunchMode.h" +#include "QObjectPtr.h" class MinecraftAccount; +class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); @@ -13,6 +14,16 @@ struct AuthSession { QString serializeUserProperties(); + enum Status { + Undetermined, + RequiresOAuth, + RequiresPassword, + RequiresProfileSetup, + PlayableOffline, + PlayableOnline, + GoneOrMigrated + } status = Undetermined; + // combined session ID QString session; // volatile auth token @@ -21,10 +32,15 @@ struct AuthSession { QString player_name; // profile ID QString uuid; - // 'msa' or 'offline', depending on account type + // 'legacy' or 'mojang', depending on account type QString user_type; - // the actual launch mode for this session - LaunchMode launchMode; + // 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; }; using AuthSessionPtr = std::shared_ptr; diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index f8131509f..aaaec6e7f 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "QObjectPtr.h" diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index e346f015b..1ed39b5ca 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,7 +55,7 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - data.internalId = QUuid::createUuid().toString(QUuid::Id128); + data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -82,8 +82,8 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) 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(QUuid::Id128); - account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -99,18 +99,22 @@ AccountState MinecraftAccount::accountState() const return data.accountState; } -QPixmap MinecraftAccount::getFace(int width, int height) const +QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { return QPixmap(); } QPixmap skin = QPixmap(8, 8); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) skin.fill(QColorConstants::Transparent); +#else + skin.fill(QColor(0, 0, 0, 0)); +#endif QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(width, height, Qt::KeepAspectRatio); + return skin.scaled(64, 64, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) @@ -179,10 +183,8 @@ void MinecraftAccount::authFailed(QString reason) data.validity_ = Validity::None; emit changed(); } break; - case AccountTaskState::STATE_WORKING: { - data.accountState = AccountState::Unchecked; - } break; case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_WORKING: case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } @@ -191,14 +193,6 @@ 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(); @@ -241,6 +235,16 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { + 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 @@ -248,7 +252,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); + session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]")); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { @@ -286,12 +290,17 @@ 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, 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 +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto bOr = [](QByteArray& array, int index, char value) { array[index] = array.at(index) | value; }; + auto bAnd = [](QByteArray& array, int index, char value) { array[index] = array.at(index) & value; }; +#else + auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; +#endif + bAnd(digest, 6, (char)0x0f); // clear version + bOr(digest, 6, (char)0x30); // set to version 3 + bAnd(digest, 8, (char)0x3f); // clear variant + bOr(digest, 8, (char)0x80); // set to IETF variant return QUuid::fromRfc4122(digest); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 24608701d..f6fcfada2 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -104,17 +104,17 @@ 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; } + [[nodiscard]] AccountType accountType() const noexcept { return data.type; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } @@ -135,7 +135,7 @@ class MinecraftAccount : public QObject, public Usable { } } - QPixmap getFace(int width = 64, int height = 64) const; + QPixmap getFace() 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 08c780694..f9d89baa2 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; } @@ -207,7 +207,6 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(capeObj.value("url"), capeOut.url)) { continue; } - capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(capeObj.value("alias"), capeOut.alias)) { continue; } @@ -290,7 +289,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; } @@ -316,7 +315,11 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) auto value = pObj.value("value"); if (value.isString()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); +#else + texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); +#endif } if (!texturePayload.isEmpty()) { @@ -331,7 +334,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; } @@ -359,7 +362,6 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Skin url is not a string"; return false; } - skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); auto maybeMeta = skin.find("metadata"); if (maybeMeta != skin.end() && maybeMeta->isObject()) { @@ -373,7 +375,6 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Cape url is not a string"; return false; } - capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); // we don't know the cape ID as it is not returned from the session server // so just fake it - changing capes is probably locked anyway :( @@ -400,7 +401,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 +464,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 1a4e9aa74..5b9809c52 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() { - m_entitlements_request_id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto uuid = QUuid::createUuid(); + m_entitlements_request_id = uuid.toString().remove('{').remove('}'); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; - auto [request, response] = Net::Download::makeByteArray(url); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); - m_request->enableAutoRetry(true); + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); + connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); m_task->start(); qDebug() << "Getting entitlements..."; } -void EntitlementsStep::onRequestDone(QByteArray* response) +void EntitlementsStep::onRequestDone() { - qCDebug(authCredentials()) << *response; + qCDebug(authCredentials()) << *m_response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(*response, m_data->minecraftEntitlement); + Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h index 72f77dabe..f20fcac08 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.h +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -17,10 +18,11 @@ class EntitlementsStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(QByteArray* response); + void onRequestDone(); 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 7b26ca468..e067bc34c 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -16,22 +16,21 @@ void GetSkinStep::perform() { QUrl url(m_data->minecraftProfile.skin.url); - auto [request, response] = Net::Download::makeByteArray(url); - m_request = request; - m_request->enableAutoRetry(true); + m_response.reset(new QByteArray()); + m_request = Net::Download::makeByteArray(url, m_response); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); + connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); m_task->start(); } -void GetSkinStep::onRequestDone(QByteArray* response) +void GetSkinStep::onRequestDone() { if (m_request->error() == QNetworkReply::NoError) - m_data->minecraftProfile.skin.data = *response; + m_data->minecraftProfile.skin.data = *m_response; emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h index 2cd74ab92..c598f05d9 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.h +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -17,9 +18,10 @@ class GetSkinStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(QByteArray* response); + void onRequestDone(); private: + std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index 4ceed8586..954f013af 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("Fetching Minecraft access token"); + return tr("Accessing Mojang services."); } void LauncherLoginStep::perform() @@ -36,40 +36,38 @@ void LauncherLoginStep::perform() { "Accept", "application/json" }, }; - auto [request, response] = Net::Upload::makeByteArray(url, requestBody.toUtf8()); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); - m_request->enableAutoRetry(true); + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); + connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); m_task->start(); qDebug() << "Getting Minecraft access token..."; } -void LauncherLoginStep::onRequestDone(QByteArray* response) +void LauncherLoginStep::onRequestDone() { - qCDebug(authCredentials()) << *response; + qCDebug(authCredentials()) << *m_response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); - if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { + if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { - 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(*response, m_data->yggdrasilToken)) { + if (!Parsers::parseMojangResponse(*m_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("Got Minecraft access token")); + emit finished(AccountTaskState::STATE_WORKING, tr("")); } diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h index 2501f5707..0b5969f2b 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.h +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" @@ -17,9 +18,10 @@ class LauncherLoginStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(QByteArray* response); + void onRequestDone(); private: + std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index 78762a32a..38ff90a47 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -66,16 +66,15 @@ void MSADeviceCodeStep::perform() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - auto [request, response] = Net::Upload::makeByteArray(url, payload); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); - m_request->enableAutoRetry(true); + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, [this, response] { deviceAuthorizationFinished(response); }); + connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); m_task->start(); } @@ -106,28 +105,28 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d } auto obj = doc.object(); return { - obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(), - obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(), + Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"), + Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), }; } -void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) +void MSADeviceCodeStep::deviceAuthorizationFinished() { - 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); + auto rsp = parseDeviceAuthorizationResponse(*m_response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); + qDebug() << *m_response; + return; + } + if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { - qWarning() << "Device authorization failed: required fields missing"; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); return; } @@ -182,11 +181,11 @@ void MSADeviceCodeStep::authenticateUser() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - auto [request, response] = Net::Upload::makeByteArray(url, payload); - m_request = request; - m_request->addHeaderProxy(std::make_unique(headers)); + m_response.reset(new QByteArray()); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); - connect(m_request.get(), &Task::finished, this, [this, response] { authenticationFinished(response); }); + connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); m_request->setNetwork(APPLICATION->network()); m_request->start(); @@ -218,16 +217,16 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) return {}; } auto obj = doc.object(); - return { obj["access_token"].toString(), - obj["token_type"].toString(), - obj["refresh_token"].toString(), - obj["expires_in"].toInt(), - obj["error"].toString(), - obj["error_description"].toString(), + return { Json::ensureString(obj, "access_token"), + Json::ensureString(obj, "token_type"), + Json::ensureString(obj, "refresh_token"), + Json::ensureInteger(obj, "expires_in"), + Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), obj.toVariantMap() }; } -void MSADeviceCodeStep::authenticationFinished(QByteArray* response) +void MSADeviceCodeStep::authenticationFinished() { if (m_request->error() == QNetworkReply::TimeoutError) { // rfc8628#section-3.5 @@ -239,7 +238,7 @@ void MSADeviceCodeStep::authenticationFinished(QByteArray* response) startPoolTimer(); return; } - auto rsp = parseAuthenticationResponse(*response); + auto rsp = parseAuthenticationResponse(*m_response); if (rsp.error == "slow_down") { // rfc8628#section-3.5 // "A variant of 'authorization_pending', the authorization request is @@ -274,5 +273,5 @@ void MSADeviceCodeStep::authenticationFinished(QByteArray* response) 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 MSA token")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got")); } diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index cfb8270d4..7f755563f 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(QByteArray* response); + void deviceAuthorizationFinished(); void startPoolTimer(); void authenticateUser(); - void authenticationFinished(QByteArray* response); + void authenticationFinished(); private: QString m_clientId; @@ -72,6 +72,7 @@ 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 51a5e5ce0..151ee4e68 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include @@ -57,14 +56,13 @@ bool isSchemeHandlerRegistered() process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); - return output.contains(APPLICATION->desktopFileName()); + return output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME); #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); - const QString registeredRunCommand = settings.value("shell/open/command/.").toString().replace("\\", "/"); - return registeredRunCommand.contains(QCoreApplication::applicationFilePath()); + return settings.contains("shell/open/command/."); #endif return true; } @@ -82,33 +80,6 @@ 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) @@ -117,7 +88,7 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { - auto replyHandler = new LoggingOAuthHttpServerReplyHandler(this); + auto replyHandler = new QOAuthHttpServerReplyHandler(this); replyHandler->setCallbackText(QString(R"XXX(