diff --git a/.clang-format b/.clang-format index a0b93351f..005a1006b 100644 --- a/.clang-format +++ b/.clang-format @@ -16,3 +16,4 @@ BraceWrapping: BreakBeforeBraces: Custom BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false +QualifierAlignment: Left diff --git a/.clang-tidy b/.clang-tidy index 6c489fb50..c5eb09533 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,23 +1,32 @@ -Checks: - - modernize-use-using - - readability-avoid-const-params-in-decls - - misc-unused-parameters, - - readability-identifier-naming +FormatStyle: file -# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. +Checks: + "bugprone-*,clang-analyzer-*,cppcoreguidelines-*,hicpp-*,misc-*,modernize-*,performance-*,portability-*,readability-*, + -*-magic-numbers, + -*-non-private-member-variables-in-classes, + -*-special-member-functions, + -bugprone-easily-swappable-parameters, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-type-static-cast-downcast, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -portability-avoid-pragma-once, + -readability-avoid-unconditional-preprocessor-if, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -readability-redundant-access-specifiers" CheckOptions: - - { key: readability-identifier-naming.ClassCase, value: PascalCase } - - { key: readability-identifier-naming.EnumCase, value: PascalCase } - - { key: readability-identifier-naming.FunctionCase, value: camelCase } - - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase } - - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase } - - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE } - - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE } - - { key: readability-identifier-naming.ClassMemberCase, value: camelCase } - - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } - - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ } - - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ } - - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ } - - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE } - - { key: readability-identifier-naming.EnumConstantCase, value: PascalCase } + misc-include-cleaner.MissingIncludes: false + readability-identifier-naming.DefaultCase: "camelBack" + readability-identifier-naming.NamespaceCase: "CamelCase" + readability-identifier-naming.ClassCase: "CamelCase" + readability-identifier-naming.ClassConstantCase: "CamelCase" + readability-identifier-naming.EnumCase: "CamelCase" + readability-identifier-naming.EnumConstantCase: "CamelCase" + readability-identifier-naming.MacroDefinitionCase: "UPPER_CASE" + readability-identifier-naming.ClassMemberPrefix: "m_" + readability-identifier-naming.StaticConstantPrefix: "s_" + readability-identifier-naming.StaticVariablePrefix: "s_" + readability-identifier-naming.GlobalConstantPrefix: "g_" + readability-implicit-bool-conversion.AllowPointerConditions: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ea1fbfdd9..3574bf20d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: File a bug report -labels: [bug] +labels: ["bug: unconfirmed", "status: needs triage"] body: - type: markdown attributes: @@ -23,14 +23,14 @@ body: - macOS - Linux - Other -- type: textarea +- type: input attributes: label: Version of Prism Launcher description: The version of Prism Launcher used in the bug report. placeholder: Prism Launcher 5.0 validations: required: true -- type: textarea +- type: input attributes: label: Version of Qt description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index 4d285047e..5e6d68e65 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -1,7 +1,7 @@ # Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst name: Request for Comment (RFC) description: Propose a larger change and start a discussion. -labels: [rfc] +labels: ["type: enhancement", "status: needs discussion", "status: needs triage"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml index ddee86b65..18a202ae1 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -1,6 +1,6 @@ name: Suggestion description: Make a suggestion -labels: [enhancement] +labels: ["type: enhancement", "status: needs triage"] body: - type: markdown attributes: diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index b21f3221e..2ce6ca955 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -27,6 +27,18 @@ runs: using: composite steps: + - name: Cleanup Qt installation on Linux + shell: bash + run: | + rm -rf "$QT_PLUGIN_PATH"/printsupport + rm -rf "$QT_PLUGIN_PATH"/sqldrivers + rm -rf "$QT_PLUGIN_PATH"/help + rm -rf "$QT_PLUGIN_PATH"/designer + rm -rf "$QT_PLUGIN_PATH"/qmltooling + rm -rf "$QT_PLUGIN_PATH"/qmlls + rm -rf "$QT_PLUGIN_PATH"/qmllint + rm -rf "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so + - name: Setup build variables shell: bash run: | @@ -78,8 +90,10 @@ runs: # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + + #disable OpenGL and Vulkan launcher features until https://github.com/VHSgunzo/sharun/issues/35 + echo "PRISMLAUNCHER_DISABLE_GLVULKAN=1" >> "$INSTALL_APPIMAGE_DIR"/.env #makes the launcher use portals for file picking - echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" > "$INSTALL_APPIMAGE_DIR"/.env + 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" @@ -121,19 +135,19 @@ runs: tar -czf ../PrismLauncher-portable.tar.gz * - name: Upload binary tarball - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher-portable.tar.gz - name: Upload AppImage - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage - name: Upload AppImage Zsync - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage.zsync diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml index 1693ca21b..1af01250f 100644 --- a/.github/actions/package/macos/action.yml +++ b/.github/actions/package/macos/action.yml @@ -96,16 +96,36 @@ runs: fi ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + - name: Create DMG + shell: bash + env: + INSTALL_DIR: install + run: | + cd ${{ env.INSTALL_DIR }} + + mkdir -p src + cp -R "Prism Launcher.app" src/ + + ln -s /Applications src/ + + hdiutil create \ + -volname "Prism Launcher ${{ inputs.version }}" \ + -srcfolder src \ + -ov -format ULMO \ + "../PrismLauncher.dmg" + - name: Make Sparkle signature shell: bash run: | if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem - signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature_zip=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature_dmg=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.dmg -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: - - :memo: Sparkle Signature (ed25519): \`$signature\` + - :memo: Sparkle Signature (ed25519): \`$signature_zip\` (ZIP) + - :memo: Sparkle Signature (ed25519): \`$signature_dmg\` (DMG) EOF else cat >> $GITHUB_STEP_SUMMARY << EOF @@ -115,7 +135,13 @@ runs: fi - name: Upload binary tarball - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher.zip + + - name: Upload disk image + uses: actions/upload-artifact@v7 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}.dmg + path: PrismLauncher.dmg diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml index 74036154a..532f3db44 100644 --- a/.github/actions/package/windows/action.yml +++ b/.github/actions/package/windows/action.yml @@ -61,7 +61,7 @@ runs: - name: Login to Azure if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} - uses: azure/login@v2 + uses: azure/login@v3 with: client-id: ${{ inputs.azure-client-id }} tenant-id: ${{ inputs.azure-tenant-id }} @@ -69,7 +69,7 @@ runs: - name: Sign executables if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} - uses: azure/artifact-signing-action@v1 + uses: azure/artifact-signing-action@v2 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher @@ -79,8 +79,8 @@ runs: 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" + 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 @@ -142,7 +142,7 @@ runs: - name: Sign installer if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} - uses: azure/artifact-signing-action@v1 + uses: azure/artifact-signing-action@v2 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher @@ -152,8 +152,8 @@ runs: ${{ 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" + 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 @@ -168,19 +168,19 @@ runs: exclude-interactive-browser-credential: true - name: Upload binary zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: install/** - name: Upload portable zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: install-portable/** - name: Upload installer - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher-Setup.exe diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index a8ecba583..b73c7509a 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -55,7 +55,7 @@ runs: # TODO(@getchoo): Get this working on MSYS2! - name: Setup ccache if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} - uses: hendrikmuhs/ccache-action@v1.2.20 + uses: hendrikmuhs/ccache-action@v1.2.23 with: variant: sccache create-symlink: ${{ runner.os != 'Windows' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 753ea19fe..fa5af702b 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -1,4 +1,5 @@ name: Setup Linux dependencies +description: Install and setup dependencies for building Prism Launcher runs: using: composite diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 43769f4fe..24ad51d8f 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -1,4 +1,5 @@ name: Setup Windows Dependencies +description: Install and setup dependencies for building Prism Launcher inputs: build-type: @@ -90,7 +91,7 @@ runs: - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - uses: actions/cache@v5.0.1 + uses: actions/cache@v5.0.5 with: path: '${{ github.workspace }}\.ccache' key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 899f654fe..9cde3307d 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -8,8 +8,7 @@ on: # the GitHub repository. This means that it should not evaluate user input in a # way that allows code injection. -permissions: - contents: read +permissions: {} jobs: backport: @@ -19,13 +18,13 @@ jobs: actions: write # for korthout/backport-action to create PR with workflow changes name: Backport Pull Request if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v4.0.0 + uses: korthout/backport-action@v4.5 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index e6b37f188..001080154 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -14,15 +14,17 @@ on: required: true type: number +permissions: {} + jobs: blocked_status: name: Check Blocked Status - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Generate token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} @@ -70,6 +72,7 @@ jobs: ' <<< "$PR_JSON")" } >> "$GITHUB_ENV" + - name: Find Blocked/Stacked PRs in body id: pr_ids run: | @@ -149,12 +152,12 @@ jobs: - name: Add 'blocked' Label if Missing id: label_blocked - if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged) + if: "(fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'status: blocked') && !fromJSON(steps.blocking_data.outputs.all_merged)" continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | - gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER" + gh -R ${{ github.repository }} issue edit --add-label 'status: blocked' "$PR_NUMBER" - name: Remove 'blocked' Label if All Dependencies Are Merged id: unlabel_blocked @@ -163,7 +166,7 @@ jobs: env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | - gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER" + gh -R ${{ github.repository }} issue edit --remove-label 'status: blocked' "$PR_NUMBER" - name: Apply 'blocking' Label to Unmerged Dependencies id: label_blocking @@ -174,7 +177,7 @@ jobs: BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }} run: | while read -r pr ; do - gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true + gh -R ${{ github.repository }} issue edit --add-label 'status: blocking' "$pr" || true done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") - name: Apply Blocking PR Status Check @@ -251,3 +254,4 @@ jobs: --body "$COMMENT_BODY" \ --create-if-none \ --edit-last + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68bb03dd3..0596906c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,29 +8,6 @@ on: merge_group: types: [checks_requested] pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/workflows/build.yml" - - ".github/actions/package/**" - - ".github/actions/setup-dependencies/**" workflow_call: inputs: build-type: @@ -47,6 +24,8 @@ on: type: string default: Debug +permissions: {} + jobs: build: name: Build (${{ matrix.artifact-name }}) @@ -54,6 +33,7 @@ jobs: environment: ${{ inputs.environment || '' }} permissions: + contents: read # Required for Azure Trusted Signing id-token: write # Required for vcpkg binary cache @@ -152,7 +132,7 @@ jobs: - name: Run tests run: | - ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE" --extra-verbose --output-on-failure + ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE" ## # PACKAGE diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 000000000..d72994c50 --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,48 @@ +name: Clang-Tidy Code Scanning + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + merge_group: + types: [checks_requested] + pull_request: + +permissions: {} + +jobs: + clang-tidy: + name: Run Clang-Tidy + + runs-on: ubuntu-latest + + permissions: + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Required for diffing later on + submodules: "true" + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Run source generators + # TODO(@getchoo): Figure out how to make this work with PCH + run: | + nix develop --command bash -c ' + cmake -B build -D Launcher_USE_PCH=OFF && cmake --build build --target autogen autorcc + ' + + # TODO: Use SARIF after https://github.com/psastras/sarif-rs/issues/638 is fixed + - name: Run clang-tidy-diff + env: + BASE_REF: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + run: | + nix develop --command bash -c ' + clang-tidy -verify-config && git diff -U0 --no-color "$BASE_REF" | clang-tidy-diff.py -p1 -quiet -only-check-in-db + ' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a6ed96863..f9705bf53 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,35 +8,18 @@ on: merge_group: types: [checks_requested] pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - "**.ui" - - # Directories - - "buildconfig/**" - - "cmake/**" - - "launcher/**" - - "libraries/**" - - "program_info/**" - - "tests/**" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/codeql/**" - - ".github/workflows/codeql.yml" - - ".github/actions/setup-dependencies/**" workflow_dispatch: +permissions: {} + jobs: CodeQL: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -58,7 +41,7 @@ jobs: - name: Configure and Build run: | - cmake --preset linux + cmake --preset linux -DLauncher_USE_PCH=OFF cmake --build --preset linux --config Debug - name: Run tests diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..7af2c1ccb --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,174 @@ +name: Development Container + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + merge_group: + types: [checks_requested] + pull_request: + workflow_dispatch: + +permissions: {} + +env: + REGISTRY: ghcr.io + +jobs: + build: + name: Build (${{ matrix.arch }}) + + permissions: + contents: read + packages: write + + outputs: + image-name: ${{ steps.image-name.outputs.image-name }} + + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + os: ubuntu-24.04-arm + - arch: amd64 + os: ubuntu-24.04 + + runs-on: ${{ matrix.os }} + + steps: + - name: Set image name + id: image-name + run: | + echo "image-name=${REGISTRY}/${GITHUB_REPOSITORY_OWNER,,}/devcontainer" >> "$GITHUB_OUTPUT" + + - name: Install Podman + uses: redhat-actions/podman-install@main + # TODO(@getchoo): Always use this when the action properly supports ARM + if: ${{ runner.arch == 'X64' || runner.arch == 'X86' }} + with: + github-token: ${{ github.token }} + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Determine metadata for image + id: image-metadata + uses: docker/metadata-action@v6 + with: + images: | + ${{ steps.image-name.outputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=latest,enable=${{ github.event.merge_group.base_ref == 'refs/heads/develop' }} + + type=sha + type=sha,format=long + type=ref,event=branch + type=ref,event=tag + + - name: Build image + id: build-image + uses: redhat-actions/buildah-build@v2 + with: + containerfiles: | + ./Containerfile + tags: ${{ steps.image-metadata.outputs.tags }} + labels: ${{ steps.image-metadata.outputs.labels }} + + - name: Push image + id: push-image + if: ${{ github.event_name != 'pull_request' }} + uses: redhat-actions/push-to-registry@v2 + with: + tags: ${{ steps.build-image.outputs.tags }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + tls-verify: true + + - name: Export image digest + if: ${{ github.event_name != 'pull_request' }} + env: + DIGEST: ${{ steps.push-image.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP"/digests + touch "$RUNNER_TEMP"/digests/"${DIGEST#sha256:}" + + - name: Upload digest artifact + if: ${{ github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v7 + with: + name: digests-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + manifest: + name: Create manifest + + needs: [ build ] + if: ${{ github.event_name != 'pull_request' }} + + permissions: + contents: read + packages: write + + runs-on: ubuntu-24.04 + + steps: + - name: Download digests + uses: actions/download-artifact@v8 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Install Podman + # TODO(@getchoo): Always use this when the action properly supports ARM + if: ${{ runner.arch == 'X64' || runner.arch == 'X86' }} + uses: redhat-actions/podman-install@main + with: + github-token: ${{ github.token }} + + - name: Login to registry + uses: redhat-actions/podman-login@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Determine metadata for manifest + id: manifest-metadata + uses: docker/metadata-action@v6 + with: + images: | + ${{ needs.build.outputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=latest,enable=${{ github.event.merge_group.base_ref == 'refs/heads/develop' }} + + type=sha + type=sha,format=long + type=ref,event=branch + type=ref,event=tag + + - name: Create manifest list + working-directory: ${{ runner.temp }}/digests + env: + IMAGE_NAME: ${{ needs.build.outputs.image-name }} + run: | + while read -r tag; do + podman manifest create "$tag" \ + $(printf "$IMAGE_NAME@sha256:%s " *) + done <<< "$DOCKER_METADATA_OUTPUT_TAGS" + + - name: Push manifest + uses: redhat-actions/push-to-registry@v2 + with: + tags: ${{ steps.manifest-metadata.outputs.tags }} + username: ${{ github.repository_owner }} + password: ${{ github.token }} + tls-verify: true diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml index 182e40984..3542a470e 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -11,19 +11,21 @@ on: required: true type: number +permissions: {} + jobs: update-blocked-status: name: Update Blocked Status - runs-on: ubuntu-latest + runs-on: ubuntu-slim # a pr that was a `blocking:` label was merged. # find the open pr's it was blocked by and trigger a refresh of their state - if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }} + if: "${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'status: blocking') }}" steps: - name: Generate token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} @@ -35,7 +37,7 @@ jobs: PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} run: | blocked_prs=$( - gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ + gh -R ${{ github.repository }} pr list --label 'status: blocked' --json 'number,body' \ | jq -c --argjson pr "$PR_NUMBER" ' reduce ( .[] | select( .body | diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 2035668f4..58f4d263a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -17,6 +17,7 @@ on: - "**.h" - "**.java" - "**.ui" + - "**.md" # Build files - "**.nix" @@ -33,7 +34,6 @@ on: # Files - "CMakeLists.txt" - - "COPYING.md" # Workflows - ".github/workflows/nix.yml" @@ -44,6 +44,7 @@ on: - "**.h" - "**.java" - "**.ui" + - "**.md" # Build files - "**.nix" @@ -60,14 +61,12 @@ on: # Files - "CMakeLists.txt" - - "COPYING.md" # Workflows - ".github/workflows/nix.yml" workflow_dispatch: -permissions: - contents: read +permissions: {} env: DEBUG: ${{ github.ref_type != 'tag' }} @@ -76,6 +75,9 @@ jobs: build: name: Build (${{ matrix.system }}) + permissions: + contents: read + strategy: fail-fast: false matrix: @@ -86,7 +88,7 @@ jobs: - os: ubuntu-22.04-arm system: aarch64-linux - - os: macos-14 + - os: macos-26 system: aarch64-darwin runs-on: ${{ matrix.os }} @@ -101,7 +103,7 @@ jobs: # For PRs - name: Setup Nix Magic Cache if: ${{ github.event_name == 'pull_request' }} - uses: DeterminateSystems/magic-nix-cache-action@v13 + uses: DeterminateSystems/magic-nix-cache-action@v14 with: diagnostic-endpoint: "" use-flakehub: false @@ -109,7 +111,7 @@ jobs: # For in-tree builds - name: Setup Cachix if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - uses: cachix/cachix-action@v16 + uses: cachix/cachix-action@v17 with: name: prismlauncher authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a7da812e..1bb1c5b50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,14 +4,16 @@ on: release: types: [ released ] -permissions: - contents: read +permissions: {} jobs: winget: name: Winget - runs-on: windows-latest + permissions: + contents: read + + runs-on: ubuntu-slim steps: - name: Publish on Winget diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd22fe05c..e332488c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,18 @@ on: tags: - "*" +permissions: {} + jobs: build_release: name: Build Release uses: ./.github/workflows/build.yml + permissions: + contents: read + # Required for Azure Trusted Signing + id-token: write + # Required for vcpkg binary cache + packages: write with: build-type: Release environment: Release @@ -16,7 +24,9 @@ jobs: create_release: needs: build_release - runs-on: ubuntu-latest + permissions: + contents: write + runs-on: ubuntu-slim outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -26,7 +36,7 @@ jobs: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") @@ -41,6 +51,7 @@ jobs: mv PrismLauncher-*.AppImage/PrismLauncher-*-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-aarch64.AppImage.zsync PrismLauncher-Linux-aarch64.AppImage.zsync mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip + mv PrismLauncher-macOS*/PrismLauncher.dmg PrismLauncher-macOS-${{ env.VERSION }}.dmg tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -83,7 +94,7 @@ jobs: - name: Create release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} @@ -111,4 +122,5 @@ jobs: PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-macOS-${{ env.VERSION }}.zip + PrismLauncher-macOS-${{ env.VERSION }}.dmg PrismLauncher-${{ env.VERSION }}.tar.gz diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index b8f8137d7..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Stale - -on: - schedule: - # run weekly on sunday - - cron: "0 0 * * 0" - workflow_dispatch: - -jobs: - label: - name: Label issues and PRs - - runs-on: ubuntu-latest - - permissions: - issues: write - pull-requests: write - - steps: - - uses: actions/stale@v10 - with: - days-before-stale: 60 - days-before-close: -1 # Don't close anything - exempt-issue-labels: rfc,nostale,help wanted - exempt-all-milestones: true - exempt-all-assignees: true - operations-per-run: 1000 - stale-issue-label: inactive - stale-pr-label: inactive diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 7f8ca2a0d..fa3e3b4d3 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -6,25 +6,30 @@ on: - cron: "0 0 * * 0" workflow_dispatch: -permissions: - contents: write - pull-requests: write +permissions: {} jobs: update-flake: if: github.repository == 'PrismLauncher/PrismLauncher' - runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31 + - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31 - uses: DeterminateSystems/update-flake-lock@v28 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" pr-labels: | - Linux - packaging - simple change + platform: Linux + area: packaging + complexity: low + priority: low + type: robot changelog:omit diff --git a/CMakeLists.txt b/CMakeLists.txt index cd4987cc7..12afdefcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22) # minimum version required by Qt +cmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` project(Launcher LANGUAGES C CXX) if(APPLE) @@ -13,6 +13,10 @@ endif() ##################################### Set CMake options ##################################### set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOGEN_ORIGIN_DEPENDS OFF) +set(CMAKE_GLOBAL_AUTOGEN_TARGET ON) +set(CMAKE_GLOBAL_AUTORCC_TARGET ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") @@ -31,81 +35,81 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -add_compile_definitions($<$:QT_NO_DEBUG>) +add_compile_definitions($<$>:QT_NO_DEBUG>) +add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x060400) +add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) -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}") +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>" - # /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}") + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard + "$<$,$>:/Gw;/Gy;/guard:cf>" + ) - # 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}") + add_link_options( + # /LTCG allows for linking wholy optimizated programs + # /MANIFEST:NO disables generating a manifest file, we instead provide our own + # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB + "$<$:/LTCG;/MANIFEST:NO;/STACK:8388608>" + ) # /GL enables whole program optimizations - # /Gw helps reduce binary size - # /Gy allows the compiler to package individual functions - # /guard:cf enables control flow guard - foreach(lang C CXX) - set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf") - endforeach() + # NOTE: With Clang, this is implemented as regular LTO and only used during linking + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options("$<$,$>:/GL>") + endif() # See https://github.com/ccache/ccache/issues/1040 - # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT - # See https://cmake.org/cmake/help/v3.25/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html - foreach(config DEBUG RELWITHDEBINFO) - foreach(lang C CXX) - set(flags_var "CMAKE_${lang}_FLAGS_${config}") - string(REGEX REPLACE "/Z[Ii]" "/Z7" ${flags_var} "${${flags_var}}") - endforeach() - endforeach() + # TODO(@getchoo): Is sccache affected by this? Would be nice to use `ProgramDatabase`.... + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") endif() else() - set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") + add_compile_options("$<$:-fstack-protector-strong;--param=ssp-buffer-size=4>") + + # Avoid re-defining _FORTIFY_SOURCE, as it can cause redefinition errors in setups that use it by default (i.e., package builds) + if(NOT (CMAKE_C_FLAGS MATCHES "-D_FORTIFY_SOURCE" OR CMAKE_CXX_FLAGS MATCHES "-D_FORTIFY_SOURCE")) + # NOTE: _FORTIFY_SOURCE requires optimizations in most newer versions of glibc + add_compile_options("$<$,$>:-D_FORTIFY_SOURCE=2>") + endif() # ATL's pack list needs more than the default 1 Mib stack on windows - if(WIN32) - set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_link_options("$<$:-Wl,--stack,8388608>") # -ffunction-sections and -fdata-sections help reduce binary size # -mguard=cf enables Control Flow Guard # TODO: Look into -gc-sections to further reduce binary size - foreach(lang C CXX) - set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf") - endforeach() + add_compile_options("$<$,$>:-ffunction-sections;-fdata-sections;-mguard=cf>") endif() endif() -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060400") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060400") - -# set CXXFLAGS for build targets -set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") - # Export compile commands for debug builds if we can (useful in LSPs like clangd) # https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) endif() +option(USE_CLANG_TIDY "Enable the use of clang-tidy during compilation" OFF) + +if(USE_CLANG_TIDY) + find_program(CLANG_TIDY clang-tidy OPTIONAL) + if(CLANG_TIDY) + message(STATUS "Using clang-tidy during compilation") + set(CLANG_TIDY_COMMAND "${CLANG_TIDY}" "--config-file=${CMAKE_SOURCE_DIR}/.clang-tidy") + set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_COMMAND}) + else() + message(WARNING "Unable to find `clang-tidy`. Not using during compilation") + endif() +endif() + option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser @@ -142,8 +146,9 @@ if(ENABLE_LTO) if(ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO TRUE) if(CMAKE_BUILD_TYPE) - if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "IPO / LTO enabled") else() message(STATUS "Not enabling IPO / LTO on debug builds") @@ -172,14 +177,15 @@ endif() ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") -set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") +set(Launcher_WIKI_URL "https://prismlauncher.org/wiki/" CACHE STRING "URL that gets opened when the user clicks 'Launcher Help'") +set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") -set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") +set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 10) +set(Launcher_VERSION_MAJOR 12) set(Launcher_VERSION_MINOR 0) -set(Launcher_VERSION_PATCH 5) +set(Launcher_VERSION_PATCH 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") @@ -413,7 +419,7 @@ if(UNIX AND APPLE) COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" --compile "${ASSETS_OUT_DIR}" --output-partial-info-plist /dev/null - --app-icon PrismLauncher + --app-icon ${Launcher_Name} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} @@ -448,7 +454,7 @@ elseif(UNIX) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png") - install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") @@ -486,7 +492,6 @@ option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. add_subdirectory(libraries/libnbtplusplus) -add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker diff --git a/CMakePresets.json b/CMakePresets.json index 5e9ee8b39..f8496acb6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -105,7 +105,7 @@ "lhs": "${hostSystemName}", "rhs": "Darwin" }, - "configurePreset": "macos" + "configurePreset": "macos" }, { "name": "macos_universal", @@ -141,7 +141,8 @@ "name": "base", "hidden": true, "output": { - "outputOnFailure": true + "outputOnFailure": true, + "verbosity": "extra" }, "execution": { "noTestsAction": "error" @@ -152,11 +153,11 @@ } } }, - { - "name": "linux", + { + "name": "linux", "displayName": "Linux", "inherits": [ - "base" + "base" ], "condition": { "type": "equals", @@ -165,11 +166,11 @@ }, "configurePreset": "linux" }, - { - "name": "macos", + { + "name": "macos", "displayName": "macOS", "inherits": [ - "base" + "base" ], "condition": { "type": "equals", @@ -178,11 +179,11 @@ }, "configurePreset": "macos" }, - { - "name": "macos_universal", + { + "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ - "base" + "base" ], "condition": { "type": "equals", @@ -191,11 +192,11 @@ }, "configurePreset": "macos_universal" }, - { - "name": "windows_mingw", + { + "name": "windows_mingw", "displayName": "Windows (MinGW)", "inherits": [ - "base" + "base" ], "condition": { "type": "equals", @@ -204,11 +205,11 @@ }, "configurePreset": "windows_mingw" }, - { - "name": "windows_msvc", + { + "name": "windows_msvc", "displayName": "Windows (MSVC)", "inherits": [ - "base" + "base" ], "condition": { "type": "equals", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8adfaec0e..f4b12d08b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,52 @@ # Contributions Guidelines +## Restrictions on Generative AI Usage (AI Policy) + +> [!NOTE] +> The following is adapted from [matplotlib's contributing guide](https://matplotlib.org/devdocs/devel/contribute.html#generative-ai) and the [Linux Kernel policy guide](https://www.kernel.org/doc./html/next/process/coding-assistants.html) + +We expect authentic engagement in our community. + +- Do not post output from Large Language Models or similar generative AI as comments on GitHub or our discord server, as such comments tend to be formulaic and low-quality content. +- If you use generative AI tools as an aid in developing code or documentation changes, ensure that you fully understand the proposed changes and can explain why they are the correct approach. + +Make sure you have added value based on your personal competency to your contributions. +Just taking some input, feeding it to an AI and posting the result is not of value to the project. +To preserve precious core developer capacity, we reserve the right to rigorously reject seemingly AI generated low-value contributions. + +### Signed-off-by and Developer Certificate of Origin + +AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer Certificate of Origin (DCO). The human submitter is responsible for: + +- Reviewing all AI-generated code +- Ensuring compliance with licensing requirements +- Adding their own Signed-off-by tag to certify the DCO +- Taking full responsibility for the contribution + +See [Signing your work](#signing-your-work) for more information. + +### Attribution + +When AI tools contribute to development, proper attribution helps track the evolving role of AI in the development process. Contributions should include an Assisted-by tag in the commit message with the following format: + +```text +Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2] +``` + +Where: + +- `AGENT_NAME` is the name of the AI tool or framework +- `MODEL_VERSION` is the specific model version used +- `[TOOL1] [TOOL2]` are optional specialized analysis tools used (e.g., coccinelle, sparse, smatch, clang-tidy) + +Basic development tools (git, gcc, make, editors) should not be listed. + +Example: + +```text +Assisted-by: Claude:claude-3-opus coccinelle sparse +``` + ## Code style All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! diff --git a/Containerfile b/Containerfile new file mode 100644 index 000000000..59595fe55 --- /dev/null +++ b/Containerfile @@ -0,0 +1,74 @@ +ARG DEBIAN_VERSION=stable-slim + +FROM docker.io/library/debian:${DEBIAN_VERSION} + +ARG QT_ABI=gcc_64 +ARG QT_ARCH= +ARG QT_HOST=linux +ARG QT_TARGET=desktop +ARG QT_VERSION=6.10.2 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get --assume-yes upgrade \ + && apt-get --assume-yes autopurge + +# Use Adoptium for Java 17 +RUN apt-get --assume-yes --no-install-recommends install \ + apt-transport-https ca-certificates curl gpg +RUN curl -L https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg +RUN echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list +RUN apt-get update + +# Install base dependencies +RUN apt-get --assume-yes --no-install-recommends install \ + # Compilers + clang lld llvm temurin-17-jdk \ + # Build system + cmake ninja-build extra-cmake-modules pkg-config \ + # Dependencies + cmark gamemode-dev libarchive-dev libcmark-dev libgamemode0 libgl1-mesa-dev libqrencode-dev libtomlplusplus-dev scdoc zlib1g-dev \ + # Tooling + clang-format clang-tidy git + +# Use LLD by default for faster linking +ENV CMAKE_LINKER_TYPE=lld + +# Prepare and install Qt +## Setup UTF-8 locale (required, apparently) +RUN apt-get --assume-yes --no-install-recommends install locales +RUN echo "C.UTF-8 UTF-8" > /etc/locale.gen +RUN locale-gen +ENV LC_ALL=C.UTF-8 + +## Some libraries are required for the official binaries +RUN apt-get --assume-yes --no-install-recommends install \ + libglib2.0-0t64 libxkbcommon0 python3-pip + +RUN pip3 install --break-system-packages aqtinstall +RUN aqt install-qt \ + ${QT_HOST} ${QT_TARGET} ${QT_VERSION} ${QT_ARCH} \ + --outputdir /opt/qt \ + --modules qtimageformats qtnetworkauth + +ENV PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/bin:$PATH +ENV QT_PLUGIN_PATH=/opt/qt/${QT_VERSION}/${QT_ABI}/plugins/ + +## We don't use these. Nuke them +RUN rm -rf \ + "$QT_PLUGIN_PATH"/designer \ + "$QT_PLUGIN_PATH"/help \ + # "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so \ + "$QT_PLUGIN_PATH"/printsupport \ + "$QT_PLUGIN_PATH"/qmllint \ + "$QT_PLUGIN_PATH"/qmlls \ + "$QT_PLUGIN_PATH"/qmltooling \ + "$QT_PLUGIN_PATH"/sqldrivers + +# Setup workspace +RUN mkdir /work +WORKDIR /work + +ENTRYPOINT ["bash"] +CMD ["-i"] diff --git a/README.md b/README.md index ce19db037..ac6cfd96b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Please understand that these builds are not intended for most users. There may b There are development builds available through: - [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contributors) -- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/build/develop) (this will always point only to the latest version of develop) +- [nightly.link](https://prismlauncher.org/nightly) (this will always point only to the latest version of develop) These have debug information in the binaries, so their file sizes are relatively larger. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 0a1ac334d..14d8236d8 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -50,6 +50,7 @@ Config::Config() LAUNCHER_GIT = "@Launcher_Git@"; LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; + LAUNCHER_ENVNAME = "@Launcher_ENVName@"; USER_AGENT = "@Launcher_UserAgent@"; @@ -105,13 +106,14 @@ Config::Config() NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; + WIKI_URL = "@Launcher_WIKI_URL@"; HELP_URL = "@Launcher_HELP_URL@"; LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; - FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; + LEGACY_FMLLIBS_BASE_URL = "@Launcher_LEGACY_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 045d987d4..a5851bfba 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -54,6 +54,7 @@ class Config { QString LAUNCHER_GIT; QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; + QString LAUNCHER_ENVNAME; /// The major version number. int VERSION_MAJOR; @@ -128,7 +129,12 @@ class Config { QString NEWS_OPEN_URL; /** - * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help + * URL that gets opened when the user clicks 'Launcher Help' + */ + QString WIKI_URL; + + /** + * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help in a dialog window */ QString HELP_URL; @@ -169,10 +175,10 @@ class Config { QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString FMLLIBS_BASE_URL; + QString LEGACY_FMLLIBS_BASE_URL; QString TRANSLATION_FILES_URL; - QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; + QString FTB_API_BASE_URL = "https://api.feed-the-beast.com/v1/modpacks/public"; QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; @@ -188,8 +194,10 @@ class Config { QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; + QString MODRINTH_DOWNLOAD_HOST = "cdn.modrinth.com"; QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; + QString FLAME_DOWNLOAD_HOST = "edge.forgecdn.net"; QString versionString() const; /** diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake deleted file mode 100644 index 51d2fb13a..000000000 --- a/cmake/CompilerWarnings.cmake +++ /dev/null @@ -1,163 +0,0 @@ -# -# Function to set compiler warnings with reasonable defaults at the project level. -# Taken from https://github.com/aminya/project_options/blob/main/src/CompilerWarnings.cmake -# under the folowing license: -# -# MIT License -# -# Copyright (c) 2022-2100 Amin Yahyaabadi -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - -include_guard() - -function(_set_project_warnings_add_target_link_option TARGET OPTIONS) - target_link_options(${_project_name} INTERFACE ${OPTIONS}) -endfunction() - -# Set the compiler warnings -# -# https://clang.llvm.org/docs/DiagnosticsReference.html -# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md -function( - set_project_warnings - _project_name - MSVC_WARNINGS - CLANG_WARNINGS - GCC_WARNINGS -) - if("${MSVC_WARNINGS}" STREQUAL "") - set(MSVC_WARNINGS - /W4 # Baseline reasonable warnings - /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data - /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data - /w14263 # 'function': member function does not override any base class virtual member function - /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not - # be destructed correctly - /w14287 # 'operator': unsigned/negative constant mismatch - /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside - # the for-loop scope - /w14296 # 'operator': expression is always 'boolean_value' - /w14311 # 'variable': pointer truncation from 'type1' to 'type2' - /w14545 # expression before comma evaluates to a function which is missing an argument list - /w14546 # function call before comma missing argument list - /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect - /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? - /w14555 # expression has no effect; expected expression with side- effect - /w14619 # pragma warning: there is no warning number 'number' - /w14640 # Enable warning on thread un-safe static member initialization - /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. - /w14905 # wide string literal cast to 'LPSTR' - /w14906 # string literal cast to 'LPWSTR' - /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied - /permissive- # standards conformance mode for MSVC compiler. - - /we4062 # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if("${CLANG_WARNINGS}" STREQUAL "") - set(CLANG_WARNINGS - -Wall - -Wextra # reasonable and standard - -Wshadow # warn the user if a variable declaration shadows one from a parent context - -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps - # catch hard to track down memory errors - -Wold-style-cast # warn for c-style casts - -Wcast-align # warn for potential performance problem casts - -Wunused # warn on anything being unused - -Woverloaded-virtual # warn if you overload (not override) a virtual function - -Wpedantic # warn if non-standard C++ is used - -Wconversion # warn on type conversions that may lose data - -Wsign-conversion # warn on sign conversions - -Wnull-dereference # warn if a null dereference is detected - -Wdouble-promotion # warn if float is implicit promoted to double - -Wformat=2 # warn on security issues around functions that format output (ie printf) - -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation - # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results - # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour - # instead of the exact standard wording so we can safely ignore it - -Wno-gnu-zero-variadic-macro-arguments - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if("${GCC_WARNINGS}" STREQUAL "") - set(GCC_WARNINGS - ${CLANG_WARNINGS} - -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist - -Wduplicated-cond # warn if if / else chain has duplicated conditions - -Wduplicated-branches # warn if if / else branches have duplicated code - -Wlogical-op # warn about logical operations being used where bitwise were probably wanted - -Wuseless-cast # warn if you perform a cast to the same type - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - endif() - - if(MSVC) - set(PROJECT_WARNINGS_CXX ${MSVC_WARNINGS}) - elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") - set(PROJECT_WARNINGS_CXX ${CLANG_WARNINGS}) - elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(PROJECT_WARNINGS_CXX ${GCC_WARNINGS}) - else() - message(AUTHOR_WARNING "No compiler warnings set for CXX compiler: '${CMAKE_CXX_COMPILER_ID}'") - # TODO support Intel compiler - endif() - - # Add C warnings - set(PROJECT_WARNINGS_C "${PROJECT_WARNINGS_CXX}") - list( - REMOVE_ITEM - PROJECT_WARNINGS_C - -Wnon-virtual-dtor - -Wold-style-cast - -Woverloaded-virtual - -Wuseless-cast - -Wextra-semi - - -Werror=switch # forbid omitting a possible value of an enum in a switch statement - ) - - target_compile_options( - ${_project_name} - INTERFACE # C++ warnings - $<$:${PROJECT_WARNINGS_CXX}> - # C warnings - $<$:${PROJECT_WARNINGS_C}> - ) - - # If we are using the compiler as a linker driver pass the warnings to it - # (most useful when using LTO or warnings as errors) - if(CMAKE_CXX_LINK_EXECUTABLE MATCHES "^") - _set_project_warnings_add_target_link_option( - ${_project_name} "$<$:${PROJECT_WARNINGS_CXX}>" - ) - endif() - - if(CMAKE_C_LINK_EXECUTABLE MATCHES "^") - _set_project_warnings_add_target_link_option( - ${_project_name} "$<$:${PROJECT_WARNINGS_C}>" - ) - endif() - - endfunction() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 0814f7ef0..eb40bacfd 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -7,7 +7,7 @@ NSMicrophoneUsageDescription A Minecraft mod wants to access your microphone. NSDownloadsFolderUsageDescription - Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears. + ${Launcher_DisplayName} uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where ${Launcher_DisplayName} scans for downloaded mods in Settings or the prompt that appears. NSLocalNetworkUsageDescription Minecraft uses the local network to find and connect to LAN servers. NSPrincipalClass @@ -61,7 +61,7 @@ mrpack CFBundleTypeName - Prism Launcher instance + ${Launcher_DisplayName} instance CFBundleTypeOSTypes TEXT @@ -87,10 +87,11 @@ CFBundleURLName - Prismlauncher + ${Launcher_Name} CFBundleURLSchemes prismlauncher + ${MACOSX_BUNDLE_EXECUTABLE_NAME} diff --git a/flake.lock b/flake.lock index 730f36a14..640d2bcf1 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "libnbtplusplus": { "flake": false, "locked": { - "lastModified": 1744811532, - "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=", + "lastModified": 1772016279, + "narHash": "sha256-7itkptyjoRcXfGLwg1/jxajetZ3a4mDc66+w4X6yW8s=", "owner": "PrismLauncher", "repo": "libnbtplusplus", - "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78", + "rev": "687e43031df0dc641984b4256bcca50d5b3f7de3", "type": "github" }, "original": { @@ -18,15 +18,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1766473571, - "narHash": "sha256-QvjEJNgMVuOootbR+DEfbiW+zSK57U32CE0jmVdcNjQ=", - "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", + "lastModified": 1778443072, + "narHash": "sha256-rNDJzV2JTV5SUTwv1cgKZYMdyoUYU9/YfegSaUf3QfY=", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.2403.76701a179d3a/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre995699.da5ad661ba4e/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, "root": { diff --git a/flake.nix b/flake.nix index 6594db522..289e0ec1c 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ }; inputs = { - nixpkgs.url = "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"; + nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; @@ -42,7 +42,7 @@ let pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_19; + llvm = pkgs.llvmPackages_22; in { @@ -85,7 +85,9 @@ let pkgs = nixpkgsFor.${system}; - llvm = pkgs.llvmPackages_19; + llvm = pkgs.llvmPackages_22; + python = pkgs.python3; + mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; }; packages' = self.packages.${system}; @@ -131,18 +133,36 @@ in { - default = pkgs.mkShell { + default = mkShell { name = "prism-launcher"; inputsFrom = [ packages'.prismlauncher-unwrapped ]; - packages = with pkgs; [ - ccache + packages = [ + pkgs.ccache llvm.clang-tools + python # NOTE(@getchoo): Required for run-clang-tidy, etc. + + (pkgs.stdenvNoCC.mkDerivation { + pname = "clang-tidy-diff"; + inherit (llvm.clang) version; + + nativeBuildInputs = [ + pkgs.installShellFiles + python.pkgs.wrapPython + ]; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + postInstall = "installBin ${llvm.libclang.python}/share/clang/clang-tidy-diff.py"; + postFixup = "wrapPythonPrograms"; + }) ]; cmakeBuildType = "Debug"; - cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags; + cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher-unwrapped.cmakeFlags; dontFixCmake = true; shellHook = '' @@ -165,16 +185,24 @@ formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); - overlays.default = final: prev: { - prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { - inherit - libnbtplusplus - self - ; - }; + overlays.default = + final: prev: - prismlauncher = final.callPackage ./nix/wrapper.nix { }; - }; + let + llvm = final.llvmPackages_22 or prev.llvmPackages_22; + in + + { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit (llvm) stdenv; + inherit + libnbtplusplus + self + ; + }; + + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; packages = forAllSystems ( system: diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c2519c634..ddeb30588 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -126,12 +126,11 @@ #include #include -#include #include "SysInfo.h" #ifdef Q_OS_LINUX #include -#include "MangoHud.h" +#include "LibraryUtils.h" #include "gamemode_client.h" #endif @@ -158,7 +157,6 @@ #endif #include #include -#include "console/WindowsConsole.h" #endif #include "console/Console.h" @@ -292,21 +290,9 @@ std::tuple read_lock_File(const Q Application::Application(int& argc, char** argv) : QApplication(argc, argv) { -#if defined Q_OS_WIN32 - // attach the parent console if stdout not already captured - if (AttachWindowsConsole()) { - consoleAttached = true; - if (auto err = EnableAnsiSupport(); !err) { - isANSIColorConsole = true; - } else { - std::cout << "Error setting up ansi console" << err.message() << std::endl; - } - } -#else if (console::isConsole()) { isANSIColorConsole = true; } -#endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); @@ -332,6 +318,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, + { "show-window", "Show the main launcher window (useful in combination with --launch)" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); // Has to be positional for some OS to handle that properly @@ -347,12 +334,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); if (parser.isSet("offline")) { - m_offline = true; + m_launchOffline = true; m_offlineName = parser.value("offline"); } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); + m_showMainWindow = parser.isSet("show-window"); for (auto url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); @@ -364,7 +352,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // error if --launch is missing with --server or --profile - if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) && + if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_launchOffline) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; @@ -406,7 +394,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } else { QDir foo; if (DesktopServices::isSnap()) { - foo = QDir(getenv("SNAP_USER_COMMON")); + foo = QDir(qEnvironmentVariable("SNAP_USER_COMMON")); } else { foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); } @@ -492,7 +480,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } - if (m_offline) { + if (m_launchOffline) { launch.args["offline_enabled"] = "true"; launch.args["offline_name"] = m_offlineName; } @@ -526,12 +514,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage("The launcher data folder is not writable!", - QString("The launcher couldn't create a log file - the data folder is not writable.\n" + QString("The launcher couldn't create a log file - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" - "(%1)\n" + "(%2)\n" "\n" "The launcher cannot continue until you fix this problem.") + .arg(logFile->errorString()) .arg(dataPath)); return; } @@ -589,16 +578,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - bool migrated = false; - - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", - "polymc.cfg"); - if (!migrated) - migrated = handleDataMigration( - dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", - "multimc.cfg"); + auto migrated = handleDataMigration( + dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", + "polymc.cfg"); + if (!migrated) { + handleDataMigration(dataPath, + FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), + "MultiMC", "multimc.cfg"); + } } { @@ -638,11 +625,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (check.write(payload) == payload.size()) { check.close(); } else { - qWarning() << "Could not write into" << liveCheckFile << "!"; + qWarning() << "Could not write into" << liveCheckFile << "error:" << check.errorString(); check.remove(); // also closes file! } } else { - qWarning() << "Could not open" << liveCheckFile << "for writing!"; + qWarning() << "Could not open" << liveCheckFile << "for writing:" << check.errorString(); } } @@ -744,8 +731,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); - m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::defaultMaxJvmMem()); m_settings->registerSetting("PermGen", 128); + m_settings->registerSetting("LowMemWarning", true); // Java Settings m_settings->registerSetting("JavaPath", ""); @@ -788,6 +776,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false); + m_settings->registerSetting("ShowModIncompat", false); + m_settings->registerSetting("DownloadGameFilesDuringInstanceCreation", true); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); @@ -828,6 +818,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); + m_settings->registerSetting("NewsGeometry", ""); + m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); @@ -862,25 +854,23 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } } { + auto resetIfInvalid = [this](const Setting* setting) { + if (const QUrl url(setting->get().toString()); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) { + m_settings->reset(setting->id()); + } + }; + // Meta URL - m_settings->registerSetting("MetaURLOverride", ""); - - QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); - - // get rid of invalid meta urls - if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) - m_settings->reset("MetaURLOverride"); + resetIfInvalid(m_settings->registerSetting("MetaURLOverride", "").get()); // Resource URL - m_settings->registerSetting("ResourceURL", BuildConfig.DEFAULT_RESOURCE_BASE); + resetIfInvalid(m_settings->registerSetting({ "ResourceURLOverride", "ResourceURL" }, "").get()); - QUrl resourceUrl(m_settings->get("ResourceURL").toString()); - - // get rid of invalid resource urls - if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) - m_settings->reset("ResourceURL"); + // Legacy FML libs URL + resetIfInvalid(m_settings->registerSetting("LegacyFMLLibsURLOverride", "").get()); } + m_settings->registerSetting("MetaRefreshOnLaunch", true); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); @@ -900,6 +890,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->set("FlameKeyOverride", flameKey); m_settings->reset("CFKeyOverride"); } + m_settings->registerSetting("FallbackMRBlockedMods", true); m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("UserAgentOverride", ""); @@ -911,7 +902,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Init page provider { - m_globalSettingsProvider = std::make_shared(tr("Settings")); + m_globalSettingsProvider = std::make_unique(tr("Settings")); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); @@ -944,15 +935,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) qInfo() << "<> Network done."; } - // load translations - { - m_translations.reset(new TranslationsModel("translations")); - auto bcp47Name = m_settings->get("Language").toString(); - m_translations->selectLanguage(bcp47Name); - qInfo() << "Your language is" << bcp47Name; - qInfo() << "<> Translations loaded."; - } - // Instance icons { auto setting = APPLICATION->settings()->getSetting("IconsDir"); @@ -992,7 +974,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } - m_instances.reset(new InstanceList(m_settings, instDir, this)); + m_instances.reset(new InstanceList(m_settings.get(), instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); qInfo() << "Loading Instances..."; m_instances->loadList(); @@ -1026,24 +1008,30 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); + m_metacache->addBase("feed", QDir("cache/feed").absolutePath()); m_metacache->Load(); qInfo() << "<> Cache initialized."; } - // now we have network, download translation updates - m_translations->downloadIndex(); + // load translations + { + m_translations.reset(new TranslationsModel("translations")); + m_translations->downloadIndex(); + qInfo() << "Your language is" << m_translations->selectedLanguage(); + qInfo() << "<> Translations loaded."; + } // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (auto profiler : m_profilers.values()) { - profiler->registerSettings(m_settings); + profiler->registerSettings(m_settings.get()); } // Create the MCEdit thing... why is this here? { - m_mcedit.reset(new MCEditTool(m_settings)); + m_mcedit.reset(new MCEditTool(m_settings.get())); } #ifdef Q_OS_MACOS @@ -1364,8 +1352,11 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); - return; + launch(inst, m_launchOffline ? LaunchMode::Offline : LaunchMode::Normal, targetToJoin, accountToUse, m_offlineName); + + if (!m_showMainWindow) { + return; + } } } if (!m_instanceIdToShowWindowOf.isEmpty()) { @@ -1418,16 +1409,6 @@ Application::~Application() { // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); - -#if defined Q_OS_WIN32 - // Detach from Windows console - if (consoleAttached) { - fclose(stdout); - fclose(stdin); - fclose(stderr); - FreeConsole(); - } -#endif } void Application::messageReceived(const QByteArray& message) @@ -1469,7 +1450,7 @@ void Application::messageReceived(const QByteArray& message) bool offline = received.args["offline_enabled"] == "true"; QString offlineName = received.args["offline_name"]; - InstancePtr instance; + BaseInstance* instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { @@ -1497,23 +1478,23 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, !offline, false, serverObject, accountObject, offlineName); + launch(instance, offline ? LaunchMode::Offline : LaunchMode::Normal, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } } -std::shared_ptr Application::translations() +TranslationsModel* Application::translations() { - return m_translations; + return m_translations.get(); } -std::shared_ptr Application::javalist() +JavaInstallList* Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } - return m_javalist; + return m_javalist.get(); } QIcon Application::logo() @@ -1532,9 +1513,8 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, - bool online, - bool demo, +bool Application::launch(BaseInstance* instance, + LaunchMode mode, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse, const QString& offlineName) @@ -1553,8 +1533,7 @@ bool Application::launch(InstancePtr instance, auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); - controller->setOnline(online); - controller->setDemo(demo); + controller->setLaunchMode(mode); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); @@ -1564,9 +1543,7 @@ bool Application::launch(InstancePtr instance, } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } - connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); - connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); - connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); + connect(controller.get(), &LaunchController::finished, this, &Application::controllerFinished); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; @@ -1580,7 +1557,7 @@ bool Application::launch(InstancePtr instance, return false; } -bool Application::kill(InstancePtr instance) +bool Application::kill(BaseInstance* instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; @@ -1589,7 +1566,7 @@ bool Application::kill(InstancePtr instance) QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive - auto controller = extras.controller; + auto& controller = extras.controller; locker.unlock(); if (controller) { return controller->abort(); @@ -1638,7 +1615,7 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -void Application::controllerSucceeded() +void Application::controllerFinished() { auto controller = qobject_cast(sender()); if (!controller) @@ -1646,10 +1623,11 @@ void Application::controllerSucceeded() auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras[id]; + auto& extras = m_instanceExtras.at(id); + const bool wasSuccessful = controller->wasSuccessful(); // on success, do... - if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { + if (wasSuccessful && controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } @@ -1659,29 +1637,8 @@ void Application::controllerSucceeded() // quit when there are no more windows. if (shouldExitNow()) { - m_status = Status::Succeeded; - exit(0); - } -} - -void Application::controllerFailed(const QString& error) -{ - Q_UNUSED(error); - auto controller = qobject_cast(sender()); - if (!controller) - return; - auto id = controller->id(); - QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras[id]; - - // on failure, do... nothing - extras.controller.reset(); - subRunningInstance(); - - // quit when there are no more windows. - if (shouldExitNow()) { - m_status = Status::Failed; - exit(1); + m_status = wasSuccessful ? Succeeded : Failed; + exit(wasSuccessful ? 0 : 1); } } @@ -1738,7 +1695,7 @@ ViewLogWindow* Application::showLogWindow() return m_viewLogWindow; } -InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) +InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString page) { if (!instance) return nullptr; @@ -1850,22 +1807,22 @@ void Application::updateProxySettings(QString proxyTypeStr, QString addr, int po qDebug() << proxyDesc; } -shared_qobject_ptr Application::metacache() +HttpMetaCache* Application::metacache() { - return m_metacache; + return m_metacache.get(); } -shared_qobject_ptr Application::network() +QNetworkAccessManager* Application::network() { - return m_network; + return m_network.get(); } -shared_qobject_ptr Application::metadataIndex() +Meta::Index* Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } - return m_metadataIndex; + return m_metadataIndex.get(); } void Application::updateCapabilities() @@ -1880,7 +1837,7 @@ void Application::updateCapabilities() if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; - if (!MangoHud::getLibraryString().isEmpty()) + if (!LibraryUtils::findMangoHud().isEmpty()) m_capabilities |= SupportsMangoHud; #endif } @@ -1888,8 +1845,8 @@ void Application::updateCapabilities() void Application::detectLibraries() { #ifdef Q_OS_LINUX - m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); - m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); + m_detectedGLFWPath = LibraryUtils::find(BuildConfig.GLFW_LIBRARY_NAME); + m_detectedOpenALPath = LibraryUtils::find(BuildConfig.OPENAL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; #endif } @@ -1996,7 +1953,7 @@ bool Application::handleDataMigration(const QString& currentData, auto setDoNotMigrate = [&nomigratePath] { QFile file(nomigratePath); if (!file.open(QIODevice::WriteOnly)) { - qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; + qWarning() << "setDoNotMigrate failed; Failed to open file" << file.fileName() << "for writing:" << file.errorString(); } }; @@ -2050,7 +2007,7 @@ void Application::triggerUpdateCheck() } } -QUrl Application::normalizeImportUrl(QString const& url) +QUrl Application::normalizeImportUrl(const QString& url) { auto local_file = QFileInfo(url); if (local_file.exists()) { diff --git a/launcher/Application.h b/launcher/Application.h index 0fd733b50..936e13d71 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -37,6 +37,8 @@ #pragma once +#include + #include #include #include @@ -44,12 +46,10 @@ #include #include #include -#include -#include +#include "QObjectPtr.h" -#include "launch/LogModel.h" -#include "minecraft/launch/MinecraftTarget.h" +#include "minecraft/auth/MinecraftAccount.h" class LaunchController; class LocalPeer; @@ -74,6 +74,12 @@ class ITheme; class MCEditTool; class ThemeManager; class IconTheme; +class BaseInstance; + +class LogModel; + +struct MinecraftTarget; +class MinecraftAccount; namespace Meta { class Index; @@ -91,7 +97,6 @@ class Index; #define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) class Application : public QApplication { - // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { StartingUp, Failed, Succeeded, Initialized }; @@ -112,7 +117,7 @@ class Application : public QApplication { bool event(QEvent* event) override; - std::shared_ptr settings() const { return m_settings; } + SettingsObject* settings() const { return m_settings.get(); } qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } @@ -120,21 +125,21 @@ class Application : public QApplication { ThemeManager* themeManager() { return m_themeManager.get(); } - shared_qobject_ptr updater() { return m_updater; } + ExternalUpdater* updater() { return m_updater.get(); } void triggerUpdateCheck(); - std::shared_ptr translations(); + TranslationsModel* translations(); - std::shared_ptr javalist(); + JavaInstallList* javalist(); - std::shared_ptr instances() const { return m_instances; } + InstanceList* instances() const { return m_instances.get(); } - std::shared_ptr icons() const { return m_icons; } + IconList* icons() const { return m_icons.get(); } MCEditTool* mcedit() const { return m_mcedit.get(); } - shared_qobject_ptr accounts() const { return m_accounts; } + AccountList* accounts() const { return m_accounts.get(); } Status status() const { return m_status; } @@ -142,11 +147,11 @@ class Application : public QApplication { void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); - shared_qobject_ptr network(); + QNetworkAccessManager* network(); - shared_qobject_ptr metacache(); + HttpMetaCache* metacache(); - shared_qobject_ptr metadataIndex(); + Meta::Index* metadataIndex(); void updateCapabilities(); @@ -182,7 +187,7 @@ class Application : public QApplication { */ bool openJsonEditor(const QString& filename); - InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); + InstanceWindow* showInstanceWindow(BaseInstance* instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); ViewLogWindow* showLogWindow(); @@ -194,7 +199,7 @@ class Application : public QApplication { bool updaterEnabled(); QString updaterBinaryName(); - QUrl normalizeImportUrl(QString const& url); + QUrl normalizeImportUrl(const QString& url); signals: void updateAllowedChanged(bool status); @@ -209,20 +214,18 @@ class Application : public QApplication { #endif public slots: - bool launch(InstancePtr instance, - bool online = true, - bool demo = false, - MinecraftTarget::Ptr targetToJoin = nullptr, - MinecraftAccountPtr accountToUse = nullptr, + bool launch(BaseInstance* instance, + LaunchMode mode = LaunchMode::Normal, + std::shared_ptr targetToJoin = nullptr, + shared_qobject_ptr accountToUse = nullptr, const QString& offlineName = QString()); - bool kill(InstancePtr instance); + bool kill(BaseInstance* instance); void closeCurrentWindow(); private slots: void on_windowClose(); void messageReceived(const QByteArray& message); - void controllerSucceeded(); - void controllerFailed(const QString& error); + void controllerFinished(); void setupWizardFinished(int status); private: @@ -238,23 +241,27 @@ class Application : public QApplication { void subRunningInstance(); bool shouldExitNow() const; + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; + private: QDateTime m_startTime; - shared_qobject_ptr m_network; + std::unique_ptr m_network; - shared_qobject_ptr m_updater; - shared_qobject_ptr m_accounts; + std::unique_ptr m_updater; + std::unique_ptr m_accounts; - shared_qobject_ptr m_metacache; - shared_qobject_ptr m_metadataIndex; + std::unique_ptr m_metacache; + std::unique_ptr m_metadataIndex; - std::shared_ptr m_settings; - std::shared_ptr m_instances; - std::shared_ptr m_icons; - std::shared_ptr m_javalist; - std::shared_ptr m_translations; - std::shared_ptr m_globalSettingsProvider; + std::unique_ptr m_settings; + std::unique_ptr m_instances; + std::unique_ptr m_icons; + std::unique_ptr m_javalist; + std::unique_ptr m_translations; + std::unique_ptr m_globalSettingsProvider; std::unique_ptr m_mcedit; QSet m_features; std::unique_ptr m_themeManager; @@ -271,15 +278,10 @@ class Application : public QApplication { Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; #endif -#if defined Q_OS_WIN32 - // used on Windows to attach the standard IO streams - bool consoleAttached = false; -#endif - // FIXME: attach to instances instead. struct InstanceXtras { InstanceWindow* window = nullptr; - shared_qobject_ptr controller; + std::unique_ptr controller; }; std::map m_instanceExtras; mutable QMutex m_instanceExtrasMutex; @@ -307,20 +309,17 @@ class Application : public QApplication { QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; - bool m_offline = false; + bool m_launchOffline = false; QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; + bool m_showMainWindow = false; std::unique_ptr logFile; - shared_qobject_ptr logModel; + std::unique_ptr logModel; public: void addQSavePath(QString); void removeQSavePath(QString); bool checkQSavePath(QString); - - private: - QHash m_qsaveResources; - mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index fdbcc11fe..0080cc516 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -45,6 +45,7 @@ #include "Application.h" #include "Json.h" +#include "launch/LaunchTask.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -53,7 +54,7 @@ #include "Commandline.h" #include "FileSystem.h" -int getConsoleMaxLines(SettingsObjectPtr settings) +int getConsoleMaxLines(SettingsObject* settings) { auto lineSetting = settings->getSetting("ConsoleMaxLines"); bool conversionOk = false; @@ -65,14 +66,14 @@ int getConsoleMaxLines(SettingsObjectPtr settings) return maxLines; } -bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) +bool shouldStopOnConsoleOverflow(SettingsObject* settings) { return settings->get("ConsoleOverflowStop").toBool(); } -BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() +BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : QObject() { - m_settings = settings; + m_settings = std::move(settings); m_global_settings = globalSettings; m_rootDir = rootDir; @@ -122,10 +123,13 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); + m_settings->registerSetting("ManagedPackURL", ""); m_settings->registerSetting("Profiler", ""); } +BaseInstance::~BaseInstance() {} + QString BaseInstance::getPreLaunchCommand() { return settings()->get("PreLaunchCommand").toString(); @@ -335,11 +339,11 @@ QString BaseInstance::instanceRoot() const return m_rootDir; } -SettingsObjectPtr BaseInstance::settings() +SettingsObject* BaseInstance::settings() { loadSpecificSettings(); - return m_settings; + return m_settings.get(); } bool BaseInstance::canLaunch() const @@ -467,9 +471,9 @@ QStringList BaseInstance::extraArguments() return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } -shared_qobject_ptr BaseInstance::getLaunchTask() +LaunchTask* BaseInstance::getLaunchTask() { - return m_launchProcess; + return m_launchProcess.get(); } void BaseInstance::updateRuntimeContext() diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index a542b76eb..9280d2e1c 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -64,9 +64,6 @@ class Task; class LaunchTask; class BaseInstance; -// pointer for lazy people -using InstancePtr = std::shared_ptr; - /// Shortcut saving target representations enum class ShortcutTarget { Desktop, Applications, Other }; @@ -78,8 +75,8 @@ struct ShortcutData { }; /// Console settings -int getConsoleMaxLines(SettingsObjectPtr settings); -bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); +int getConsoleMaxLines(SettingsObject* settings); +bool shouldStopOnConsoleOverflow(SettingsObject* settings); /*! * \brief Base class for instances. @@ -89,11 +86,11 @@ bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); * To create a new instance type, create a new class inheriting from this class * and implement the pure virtual functions. */ -class BaseInstance : public QObject, public std::enable_shared_from_this { +class BaseInstance : public QObject { Q_OBJECT protected: /// no-touchy! - BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); public: /* types */ enum class Status { @@ -103,7 +100,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this createUpdateTask() = 0; /// returns a valid launcher (task container) - virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; + virtual LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) - shared_qobject_ptr getLaunchTask(); + LaunchTask* getLaunchTask(); /*! * Create envrironment variables for running the instance @@ -286,7 +283,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this); + void launchTaskChanged(LaunchTask*); void runningStatusChanged(bool running); @@ -310,10 +307,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_this m_settings; // InstanceFlags m_flags; bool m_isRunning = false; - shared_qobject_ptr m_launchProcess; + std::unique_ptr m_launchProcess; QDateTime m_timeStarted; RuntimeContext m_runtimeContext; @@ -323,7 +320,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this; virtual ~BaseVersion() {} /*! diff --git a/launcher/BaseVersionList.h b/launcher/BaseVersionList.h index 673d13562..ba546e955 100644 --- a/launcher/BaseVersionList.h +++ b/launcher/BaseVersionList.h @@ -63,7 +63,7 @@ class BaseVersionList : public QAbstractListModel { * The task returned by this function should reset the model when it's done. * \return A pointer to a task that reloads the version list. */ - virtual Task::Ptr getLoadTask() = 0; + virtual Task::Ptr getLoadTask(bool forceReload = false) = 0; //! Checks whether or not the list is loaded. If this returns false, the list should be // loaded. diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index de647455a..7d4430fd2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -75,9 +75,6 @@ set(CORE_SOURCES # RW lock protected map RWStorage.h - # A variable that has an implicit default value and keeps track of changes - DefaultVariable.h - # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms QObjectPtr.h @@ -110,9 +107,9 @@ if (UNIX AND NOT CYGWIN AND NOT APPLE) set(CORE_SOURCES ${CORE_SOURCES} - # MangoHud - MangoHud.h - MangoHud.cpp + # LibraryUtils + LibraryUtils.h + LibraryUtils.cpp ) endif() @@ -122,6 +119,7 @@ set(NET_SOURCES net/ChecksumValidator.h net/Download.cpp net/Download.h + net/DummySink.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp @@ -246,15 +244,13 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h - minecraft/auth/steps/XboxProfileStep.cpp - minecraft/auth/steps/XboxProfileStep.h minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp - minecraft/update/FMLLibrariesTask.cpp - minecraft/update/FMLLibrariesTask.h + minecraft/update/LegacyFMLLibrariesTask.cpp + minecraft/update/LegacyFMLLibrariesTask.h minecraft/update/FoldersTask.cpp minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp @@ -264,6 +260,8 @@ 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 @@ -531,6 +529,11 @@ set(FTB_SOURCES modplatform/import_ftb/PackInstallTask.cpp modplatform/import_ftb/PackHelpers.h modplatform/import_ftb/PackHelpers.cpp + + modplatform/ftb/FTBPackInstallTask.h + modplatform/ftb/FTBPackInstallTask.cpp + modplatform/ftb/FTBPackManifest.h + modplatform/ftb/FTBPackManifest.cpp ) set(FLAME_SOURCES @@ -796,6 +799,8 @@ SET(LAUNCHER_SOURCES ApplicationMessage.cpp SysInfo.h SysInfo.cpp + HardwareInfo.cpp + HardwareInfo.h # console utils console/Console.h @@ -812,23 +817,6 @@ SET(LAUNCHER_SOURCES KonamiCode.h KonamiCode.cpp - # Bundled resources - resources/backgrounds/backgrounds.qrc - resources/multimc/multimc.qrc - resources/pe_dark/pe_dark.qrc - resources/pe_light/pe_light.qrc - resources/pe_colored/pe_colored.qrc - resources/pe_blue/pe_blue.qrc - resources/breeze_dark/breeze_dark.qrc - resources/breeze_light/breeze_light.qrc - resources/OSX/OSX.qrc - resources/iOS/iOS.qrc - resources/flat/flat.qrc - resources/flat_white/flat_white.qrc - resources/documents/documents.qrc - resources/shaders/shaders.qrc - "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" - # Icons icons/MMCIcon.h icons/MMCIcon.cpp @@ -898,6 +886,7 @@ SET(LAUNCHER_SOURCES ui/themes/CatPainter.h # Processes + LaunchMode.h LaunchController.h LaunchController.cpp @@ -1008,6 +997,13 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h + ui/pages/modplatform/ftb/FtbFilterModel.cpp + ui/pages/modplatform/ftb/FtbFilterModel.h + ui/pages/modplatform/ftb/FtbListModel.cpp + ui/pages/modplatform/ftb/FtbListModel.h + ui/pages/modplatform/ftb/FtbPage.cpp + ui/pages/modplatform/ftb/FtbPage.h + ui/pages/modplatform/legacy_ftb/Page.cpp ui/pages/modplatform/legacy_ftb/Page.h ui/pages/modplatform/legacy_ftb/ListModel.h @@ -1071,6 +1067,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ImportResourceDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h + ui/dialogs/NetworkJobFailedDialog.cpp + ui/dialogs/NetworkJobFailedDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp @@ -1208,76 +1206,6 @@ if(WIN32) ) endif() -qt_wrap_ui(LAUNCHER_UI - ui/MainWindow.ui - ui/setupwizard/PasteWizardPage.ui - ui/setupwizard/AutoJavaWizardPage.ui - ui/setupwizard/LoginWizardPage.ui - ui/pages/global/AccountListPage.ui - ui/pages/global/JavaPage.ui - ui/pages/global/LauncherPage.ui - ui/pages/global/APIPage.ui - ui/pages/global/ProxyPage.ui - ui/pages/global/ExternalToolsPage.ui - ui/pages/instance/ExternalResourcesPage.ui - ui/pages/instance/NotesPage.ui - ui/pages/instance/LogPage.ui - ui/pages/instance/ServersPage.ui - ui/pages/instance/OtherLogsPage.ui - ui/pages/instance/VersionPage.ui - ui/pages/instance/ManagedPackPage.ui - ui/pages/instance/WorldListPage.ui - ui/pages/instance/ScreenshotsPage.ui - ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui - ui/pages/modplatform/atlauncher/AtlPage.ui - ui/pages/modplatform/CustomPage.ui - ui/pages/modplatform/ResourcePage.ui - ui/pages/modplatform/flame/FlamePage.ui - ui/pages/modplatform/legacy_ftb/Page.ui - ui/pages/modplatform/import_ftb/ImportFTBPage.ui - ui/pages/modplatform/ImportPage.ui - ui/pages/modplatform/OptionalModDialog.ui - ui/pages/modplatform/modrinth/ModrinthPage.ui - ui/pages/modplatform/technic/TechnicPage.ui - ui/widgets/CustomCommands.ui - ui/widgets/EnvironmentVariables.ui - ui/widgets/InfoFrame.ui - ui/widgets/ModFilterWidget.ui - ui/widgets/SubTaskProgressBar.ui - ui/widgets/AppearanceWidget.ui - ui/widgets/MinecraftSettingsWidget.ui - ui/widgets/JavaSettingsWidget.ui - ui/dialogs/CopyInstanceDialog.ui - ui/dialogs/CreateShortcutDialog.ui - ui/dialogs/ProfileSetupDialog.ui - ui/dialogs/ProgressDialog.ui - ui/dialogs/NewInstanceDialog.ui - ui/dialogs/NewComponentDialog.ui - ui/dialogs/NewsDialog.ui - ui/dialogs/ProfileSelectDialog.ui - ui/dialogs/ExportInstanceDialog.ui - ui/dialogs/ExportPackDialog.ui - ui/dialogs/ExportToModListDialog.ui - ui/dialogs/IconPickerDialog.ui - ui/dialogs/ImportResourceDialog.ui - ui/dialogs/MSALoginDialog.ui - ui/dialogs/AboutDialog.ui - ui/dialogs/ReviewMessageBox.ui - ui/dialogs/ScrollMessageBox.ui - ui/dialogs/BlockedModsDialog.ui - ui/dialogs/ChooseProviderDialog.ui - ui/dialogs/skins/SkinManageDialog.ui - ui/dialogs/ChooseOfflineNameDialog.ui -) - -qt_wrap_ui(PRISM_UPDATE_UI - ui/dialogs/UpdateAvailableDialog.ui -) - -if (NOT Apple) - set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) -endif() - qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc @@ -1290,24 +1218,17 @@ qt_add_resources(LAUNCHER_RESOURCES resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc + resources/flat_white/flat_white.qrc resources/documents/documents.qrc resources/shaders/shaders.qrc "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" ) -qt_wrap_ui(PRISMUPDATER_UI - updater/prismupdater/SelectReleaseDialog.ui - ui/widgets/SubTaskProgressBar.ui - ui/dialogs/ProgressDialog.ui -) - ######## Windows resource files ######## if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() -include(CompilerWarnings) - ######## Precompiled Headers ########### if(${Launcher_USE_PCH}) @@ -1322,11 +1243,7 @@ endif() ####### Targets ######## # Add executable -add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) -set_project_warnings(Launcher_logic - "${Launcher_MSVC_WARNINGS}" - "${Launcher_CLANG_WARNINGS}" - "${Launcher_GCC_WARNINGS}") +add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_RESOURCES}) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) @@ -1335,7 +1252,6 @@ if(${Launcher_USE_PCH}) endif() target_link_libraries(Launcher_logic - systeminfo Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} @@ -1412,8 +1328,6 @@ if(APPLE) endif() endif() -target_link_libraries(Launcher_logic) - add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) if(${Launcher_USE_PCH}) @@ -1449,7 +1363,7 @@ endif() if(Launcher_BUILD_UPDATER) # Updater - add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) + add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(${Launcher_USE_PCH}) @@ -1458,7 +1372,6 @@ if(Launcher_BUILD_UPDATER) target_link_libraries(prism_updater_logic ${ZLIB_LIBRARIES} - systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core @@ -1503,10 +1416,6 @@ 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}) @@ -1515,7 +1424,6 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) endif() target_link_libraries(filelink_logic - systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core @@ -1567,6 +1475,28 @@ if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) endif() +# Set basic compiler warning/error flags for all targets +get_property(Launcher_TARGETS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) +foreach(target ${Launcher_TARGETS}) + message(STATUS "Enabling all warnings as errors for target '${target}'") + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(${target} PRIVATE /W4 /WX /permissive-) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror) + endif() +endforeach() + +# Disable some warnings in main launcher target due to being present in a lot of places. TODO: Fix them. +if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter + target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter +else() + # sfinae-incomplete is a new GCC warning and triggers in Qt headers + # no-unknown-warning-option so that compilers that don't have sfinae-incomplete don't error + target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) + target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-unknown-warning-option -Wno-sfinae-incomplete) +endif() + #### The bundle mess! #### # Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 9677f868e..cab22089e 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -63,7 +63,7 @@ void DataMigrationTask::dryRunFinished() void DataMigrationTask::dryRunAborted() { - emitFailed(tr("Aborted")); + emitAborted(); } void DataMigrationTask::copyFinished() @@ -81,5 +81,5 @@ void DataMigrationTask::copyFinished() void DataMigrationTask::copyAborted() { - emitFailed(tr("Aborted")); + emitAborted(); } diff --git a/launcher/DefaultVariable.h b/launcher/DefaultVariable.h deleted file mode 100644 index b082091c7..000000000 --- a/launcher/DefaultVariable.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -template -class DefaultVariable { - public: - DefaultVariable(const T& value) { defaultValue = value; } - DefaultVariable& operator=(const T& value) - { - currentValue = value; - is_default = currentValue == defaultValue; - is_explicit = true; - return *this; - } - operator const T&() const { return is_default ? defaultValue : currentValue; } - bool isDefault() const { return is_default; } - bool isExplicit() const { return is_explicit; } - - private: - T currentValue; - T defaultValue; - bool is_default = true; - bool is_explicit = false; -}; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 9ca1c5fa6..ef56e3e65 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -36,6 +36,7 @@ */ #include "FileSystem.h" +#include #include #include "BuildConfig.h" @@ -282,6 +283,9 @@ bool copyFileAttributes(QString src, QString dst) if (attrs == INVALID_FILE_ATTRIBUTES) return false; return SetFileAttributesW(dst.toStdWString().c_str(), attrs); +#else + Q_UNUSED(src); + Q_UNUSED(dst); #endif return true; } @@ -680,6 +684,32 @@ bool deletePath(QString path) return err.value() == 0; } +bool deleteContents(const QString& path) +{ + const QFileInfo info(path); + if (!info.exists()) { + return true; + } + if (!info.isDir()) { + qWarning() << "Attempted to delete contents of non-directory path:" << path; + return false; + } + + bool ret = true; + + for (const auto& entry : fs::directory_iterator(StringUtils::toStdString(path))) { + std::error_code err; + + fs::remove_all(entry.path(), err); + if (err.value() != 0) { + qWarning().nospace() << "Could not delete directory entry " << entry.path() << ": " << QString::fromStdString(err.message()); + ret = false; + } + } + + return ret; +} + bool trash(QString path, QString* pathInTrash) { // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal @@ -793,68 +823,33 @@ QString NormalizePath(QString path) } } -static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; -static const QString BAD_NTFS_CHARS = "<>:\"|?*"; -static const QString BAD_HFS_CHARS = ":"; - -static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; - -QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +namespace { +const QString g_badChars = "<>:\"|?*\r\n!"; +QString removeChars(QString source, QChar replace, const QString& extraChars = "") { - for (int i = 0; i < string.length(); i++) - if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) - string[i] = replaceWith; - return string; -} - -QString RemoveInvalidPathChars(QString path, QChar replaceWith) -{ - QString invalidChars; -#ifdef Q_OS_WIN - invalidChars = BAD_WIN_CHARS; -#endif - - // the null character is ignored in this check as it was not a problem until now - switch (statFS(path).fsType) { - case FilesystemType::FAT: // similar to NTFS - /* fallthrough */ - case FilesystemType::NTFS: - /* fallthrough */ - case FilesystemType::REFS: // similar to NTFS(should be available only on windows) - invalidChars += BAD_NTFS_CHARS; - break; - // case FilesystemType::EXT: - // case FilesystemType::EXT_2_OLD: - // case FilesystemType::EXT_2_3_4: - // case FilesystemType::XFS: - // case FilesystemType::BTRFS: - // case FilesystemType::NFS: - // case FilesystemType::ZFS: - case FilesystemType::APFS: - /* fallthrough */ - case FilesystemType::HFS: - /* fallthrough */ - case FilesystemType::HFSPLUS: - /* fallthrough */ - case FilesystemType::HFSX: - invalidChars += BAD_HFS_CHARS; - break; - // case FilesystemType::FUSEBLK: - // case FilesystemType::F2FS: - // case FilesystemType::UNKNOWN: - default: - break; + auto badChars = g_badChars; + if (!extraChars.isEmpty()) { + badChars += extraChars; } - if (invalidChars.size() != 0) { - for (int i = 0; i < path.length(); i++) { - if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { - path[i] = replaceWith; - } + for (auto& c : source) { + if (c.unicode() < 0x20 || !c.isPrint() || badChars.contains(c)) { + c = replace; } } - return path; + return source; +} +} // namespace + +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +{ + return removeChars(std::move(string), replaceWith, "\\/"); +} + +QString RemoveInvalidPathChars(QString string, QChar replaceWith) +{ + return removeChars(std::move(string), replaceWith); } QString DirNameFromString(QString string, QString inDir) @@ -950,7 +945,10 @@ QString createShortcut(QString destination, QString target, QStringList args, QS qWarning() << "Couldn't create directories within application"; return QString(); } - info.open(QIODevice::WriteOnly | QIODevice::Text); + if (!info.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Failed to open file" << info.fileName() << "for writing:" << info.errorString(); + return QString(); + } QFile(icon).rename(resources.path() + "/Icon.icns"); @@ -958,7 +956,10 @@ QString createShortcut(QString destination, QString target, QStringList args, QS QString exec = binaryDir.path() + "/Run.command"; QFile f(exec); - f.open(QIODevice::WriteOnly | QIODevice::Text); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); + return QString(); + } QTextStream stream(&f); auto argstring = quoteArgs(args, "\"", "\\\""); @@ -1001,7 +1002,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS destination += ".desktop"; QFile f(destination); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { - qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; + qWarning() << "Failed to open file" << f.fileName() << "for writing:" << f.errorString(); return QString(); } QTextStream stream(&f); diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index f2676b147..6d9b01178 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -291,6 +291,13 @@ bool move(const QString& source, const QString& dest); */ bool deletePath(QString path); +/** + * Delete a folder's contents recursively but not the folder itself. + * @param path The path to the folder. + * @return Whether the deletion was completely successful. + */ +bool deleteContents(const QString& path); + bool removeFiles(QStringList listFile); /** diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index dc786e10e..201dcd572 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -172,7 +172,8 @@ int inf(QFile* source, std::function handleBlock) assert(ret != Z_STREAM_ERROR); /* state not clobbered */ switch (ret) { case Z_NEED_DICT: - ret = Z_DATA_ERROR; /* and fall through */ + ret = Z_DATA_ERROR; + [[fallthrough]]; case Z_DATA_ERROR: case Z_MEM_ERROR: (void)inflateEnd(&strm); diff --git a/launcher/HardwareInfo.cpp b/launcher/HardwareInfo.cpp new file mode 100644 index 000000000..36b6f7783 --- /dev/null +++ b/launcher/HardwareInfo.cpp @@ -0,0 +1,355 @@ +// 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 new file mode 100644 index 000000000..4efd339b6 --- /dev/null +++ b/launcher/HardwareInfo.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace HardwareInfo { +QString cpuInfo(); +uint64_t totalRamMiB(); +uint64_t availableRamMiB(); +QStringList gpuInfo(); +} // namespace HardwareInfo + +#ifdef Q_OS_MACOS +namespace MacOSHardwareInfo { +enum class MemoryPressureLevel : uint8_t { + Normal = 1, + Warning = 2, + Critical = 4, +}; + +MemoryPressureLevel memoryPressureLevel(); +QString memoryPressureLevelName(); +} // namespace MacOSHardwareInfo +#endif \ No newline at end of file diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 4bb57bc58..e32cdf095 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -8,7 +8,7 @@ #include "settings/INISettingsObject.h" #include "tasks/Task.h" -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) +InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); @@ -146,9 +146,9 @@ void InstanceCopyTask::copyFinished() } // FIXME: shouldn't this be able to report errors? - auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + auto instanceSettings = std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")); - InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); + BaseInstance* inst(new NullInstance(m_globalSettings, std::move(instanceSettings), m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); if (!m_keepPlaytime) { diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index ef4120bc6..a926af8a7 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -15,7 +15,7 @@ class InstanceCopyTask : public InstanceTask { Q_OBJECT public: - explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); + explicit InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. @@ -26,7 +26,7 @@ class InstanceCopyTask : public InstanceTask { private: /* data */ - InstancePtr m_origInstance; + BaseInstance* m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; Filter m_matcher; diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 94c229128..e58926660 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -2,7 +2,25 @@ #include #include -#include "FileSystem.h" + +#include "Application.h" +#include "InstanceTask.h" +#include "minecraft/MinecraftLoadAndCheck.h" +#include "tasks/SequentialTask.h" + +bool InstanceCreationTask::abort() +{ + if (!canAbort()) { + return false; + } + + m_abort = true; + if (m_gameFilesTask) { + return m_gameFilesTask->abort(); + } + + return InstanceTask::abort(); +} void InstanceCreationTask::executeTask() { @@ -19,9 +37,11 @@ void InstanceCreationTask::executeTask() return; } - if (!createInstance()) { - if (m_abort) + m_instance = createInstance(); + if (!m_instance) { + if (m_abort) { return; + } qWarning() << "Instance creation failed!"; if (!m_error_message.isEmpty()) { @@ -44,9 +64,10 @@ void InstanceCreationTask::executeTask() setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (const QString& path : m_files_to_remove) { - if (!QFile::exists(path)) + for (const QString& path : m_filesToRemove) { + if (!QFile::exists(path)) { continue; + } qDebug() << "Removing" << path; @@ -61,6 +82,61 @@ void InstanceCreationTask::executeTask() return; } } - if (!m_abort) - emitSucceeded(); + + if (!m_abort) { + if (!APPLICATION->settings()->get("DownloadGameFilesDuringInstanceCreation").toBool()) { + emitSucceeded(); + return; + } + setAbortable(true); + setAbortButtonText(tr("Skip")); + qDebug() << "Downloading game files"; + + auto updateTasks = m_instance->createUpdateTask(); + if (updateTasks.isEmpty()) { + emitSucceeded(); + return; + } + auto task = makeShared(); + task->addTask(makeShared(m_instance.get(), Net::Mode::Online)); + for (const auto& t : updateTasks) { + task->addTask(t); + } + connect(task.get(), &Task::finished, this, [this, task] { + if (task->wasSuccessful() || m_abort) { + emitSucceeded(); + } else { + emitFailed(tr("Could not download game files: %1").arg(task->failReason())); + } + }); + propagateFromOther(task.get()); + setDetails(tr("Downloading game files")); + + m_gameFilesTask = task; + m_gameFilesTask->start(); + } +} + +void InstanceCreationTask::scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled) +{ + if (path.isEmpty()) { + return; + } + if (path.startsWith("saves/")) { + if (m_shouldDeleteSaves == ShouldDeleteSaves::NotAsked) { + m_shouldDeleteSaves = askIfShouldDeleteSaves(parent); + } + if (m_shouldDeleteSaves == ShouldDeleteSaves::No) { + return; + } + } + qDebug() << "Scheduling" << path << "for removal"; + m_filesToRemove.append(dir.absoluteFilePath(path)); + if (checkDisabled) { + if (path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_filesToRemove.append(dir.absoluteFilePath(path.chopped(9))); + } else { + m_filesToRemove.append(dir.absoluteFilePath(path + ".disabled")); + } + } } diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h index 84fb2a145..39acaf8b2 100644 --- a/launcher/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -2,6 +2,7 @@ #include "BaseVersion.h" #include "InstanceTask.h" +#include "minecraft/MinecraftInstance.h" class InstanceCreationTask : public InstanceTask { Q_OBJECT @@ -9,6 +10,8 @@ class InstanceCreationTask : public InstanceTask { InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; + bool abort() override; + protected: void executeTask() final override; @@ -27,20 +30,24 @@ class InstanceCreationTask : public InstanceTask { /** * Creates a new instance. * - * Returns whether the instance creation was successful (true) or not (false). + * Returns the instance if it was created or nullptr otherwise. */ - virtual bool createInstance() { return false; }; + virtual std::unique_ptr createInstance() { return nullptr; } QString getError() const { return m_error_message; } protected: void setError(const QString& message) { m_error_message = message; }; + void scheduleToDelete(QWidget* parent, const QDir& dir, const QString& path, bool checkDisabled = false); protected: bool m_abort = false; - QStringList m_files_to_remove; + QStringList m_filesToRemove; + ShouldDeleteSaves m_shouldDeleteSaves; private: QString m_error_message; + std::unique_ptr m_instance; + Task::Ptr m_gameFilesTask; }; diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp index 8be0dccac..75fbdb6c6 100644 --- a/launcher/InstanceDirUpdate.cpp +++ b/launcher/InstanceDirUpdate.cpp @@ -42,7 +42,7 @@ #include "InstanceList.h" #include "ui/dialogs/CustomMessageBox.h" -QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent) +QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent) { if (oldName == newName) return QString(); diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h index b92a59c4c..9da49a9a6 100644 --- a/launcher/InstanceDirUpdate.h +++ b/launcher/InstanceDirUpdate.h @@ -37,7 +37,7 @@ #include "BaseInstance.h" /// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened -QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent); +QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent); /// Check if there are linked instances, and display a warning; return true if the operation should proceed bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index cd168dee2..9b04f99b6 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -322,6 +322,7 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); @@ -341,9 +342,9 @@ void InstanceImportTask::processTechnic() void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(configPath); + auto instanceSettings = std::make_unique(configPath); - NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + NullInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); // reset time played on import... because packs. instance.resetTimePlayed(); @@ -421,6 +422,7 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::abortButtonTextChanged, this, &Task::setAbortButtonText); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index de94db7c3..1339499c7 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -34,42 +34,37 @@ * limitations under the License. */ +#include "InstanceList.h" + #include -#include #include #include #include -#include #include #include #include -#include #include #include -#include -#include #include #include -#include #include "BaseInstance.h" #include "ExponentialSeries.h" #include "FileSystem.h" -#include "InstanceList.h" + #include "InstanceTask.h" #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ShortcutUtils.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 -#include +#include #endif const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) +InstanceList::InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); @@ -143,7 +138,7 @@ QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const QStringList InstanceList::getLinkedInstancesById(const QString& id) const { QStringList linkedInstances; - for (auto inst : m_instances) { + for (auto& inst : m_instances) { if (inst->isLinkedToInstanceId(id)) linkedInstances.append(inst->id()); } @@ -153,15 +148,15 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return m_instances.count(); + return count(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); - if (row < 0 || row >= m_instances.size()) + if (row < 0 || row >= count()) return QModelIndex(); - return createIndex(row, column, (void*)m_instances.at(row).get()); + return createIndex(row, column, m_instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const @@ -266,7 +261,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) if (changed) { increaseGroupCount(name); - auto idx = getInstIndex(inst.get()); + auto idx = getInstIndex(inst); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } @@ -457,7 +452,7 @@ void InstanceList::deleteInstance(const InstanceId& id) } } -static QMap getIdMapping(const QList& list) +static QMap getIdMapping(const std::vector>& list) { QMap out; int i = 0; @@ -466,7 +461,7 @@ static QMap getIdMapping(const QList& if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } - out[id] = std::make_pair(item, i); + out[id] = std::make_pair(item.get(), i); i++; } return out; @@ -504,17 +499,16 @@ InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); - QList newList; + std::vector> newList; for (auto& id : discoverInstances()) { if (existingIds.contains(id)) { - auto instPair = existingIds[id]; existingIds.remove(id); qInfo() << "Should keep and soft-reload" << id; } else { - InstancePtr instPtr = loadInstance(id); + std::unique_ptr instPtr = loadInstance(id); if (instPtr) { - newList.append(instPtr); + newList.push_back(std::move(instPtr)); } } } @@ -566,8 +560,8 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for (auto const& itr : m_instances) { - totalPlayTime += itr.get()->totalTimePlayed(); + for (const auto& itr : m_instances) { + totalPlayTime += itr->totalTimePlayed(); } } @@ -578,12 +572,12 @@ void InstanceList::saveNow() } } -void InstanceList::add(const QList& t) +void InstanceList::add(std::vector>& t) { - beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); - m_instances.append(t); + beginInsertRows(QModelIndex(), count(), static_cast(count() + t.size() - 1)); for (auto& ptr : t) { - connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + m_instances.push_back(std::move(ptr)); + connect(m_instances.back().get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } @@ -613,26 +607,26 @@ void InstanceList::providerUpdated() } } -InstancePtr InstanceList::getInstanceById(QString instId) const +BaseInstance* InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) - return InstancePtr(); + return nullptr; for (auto& inst : m_instances) { if (inst->id() == instId) { - return inst; + return inst.get(); } } - return InstancePtr(); + return nullptr; } -InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const +BaseInstance* InstanceList::getInstanceByManagedName(const QString& managed_name) const { if (managed_name.isEmpty()) return {}; - for (auto instance : m_instances) { + for (auto& instance : m_instances) { if (instance->getManagedPackName() == managed_name) - return instance; + return instance.get(); } return {}; @@ -640,14 +634,14 @@ InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { - return index(getInstIndex(getInstanceById(id).get())); + return index(getInstIndex(getInstanceById(id))); } int InstanceList::getInstIndex(BaseInstance* inst) const { - int count = m_instances.count(); + int count = this->count(); for (int i = 0; i < count; i++) { - if (inst == m_instances[i].get()) { + if (inst == m_instances.at(i).get()) { return i; } } @@ -663,15 +657,15 @@ void InstanceList::propertiesChanged(BaseInstance* inst) } } -InstancePtr InstanceList::loadInstance(const InstanceId& id) +std::unique_ptr InstanceList::loadInstance(const InstanceId& id) { if (!m_groupsLoaded) { loadGroupList(); } auto instanceRoot = FS::PathCombine(m_instDir, id); - auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); - InstancePtr inst; + auto instanceSettings = std::make_unique(FS::PathCombine(instanceRoot, "instance.cfg")); + std::unique_ptr inst; instanceSettings->registerSetting("InstanceType", ""); @@ -680,9 +674,9 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { - inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + inst.reset(new MinecraftInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } else { - inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + inst.reset(new NullInstance(m_globalSettings, std::move(instanceSettings), instanceRoot)); } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); @@ -911,20 +905,20 @@ class InstanceStaging : public Task { const unsigned maxBackoff = 16; public: - InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) - : m_parent(parent), backoff(minBackoff, maxBackoff) + InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObject* settings) : m_parent(parent), backoff(minBackoff, maxBackoff) { m_stagingPath = parent->getStagedInstancePath(); m_child.reset(child); m_child->setStagingPath(m_stagingPath); - m_child->setParentSettings(std::move(settings)); + m_child->setParentSettings(settings); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); + connect(child, &Task::abortButtonTextChanged, this, &InstanceStaging::setAbortButtonText); connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::progress, this, &InstanceStaging::setProgress); @@ -932,22 +926,21 @@ class InstanceStaging : public Task { connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } - virtual ~InstanceStaging() {} + ~InstanceStaging() override = default; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if (!canAbort()) + if (!canAbort()) { return false; + } - m_child->abort(); - - return Task::abort(); + return m_child->abort(); } bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: - virtual void executeTask() override + void executeTask() override { if (m_stagingPath.isNull()) { emitFailed(tr("Could not create staging folder")); @@ -962,12 +955,14 @@ class InstanceStaging : public Task { void childSucceeded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { + if (m_parent->commitStagedInstance(m_stagingPath, *m_child, m_child->group(), *m_child)) { + m_backoffTimer.stop(); emitSucceeded(); return; } // we actually failed, retry? if (sleepTime == maxBackoff) { + m_backoffTimer.stop(); emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } @@ -976,12 +971,14 @@ class InstanceStaging : public Task { } void childFailed(const QString& reason) { + m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } void childAborted() { + m_backoffTimer.stop(); m_parent->destroyStagingPath(m_stagingPath); emitAborted(); } @@ -995,7 +992,7 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - unique_qobject_ptr m_child; + std::unique_ptr m_child; QTimer m_backoffTimer; }; @@ -1028,15 +1025,14 @@ QString InstanceList::getStagedInstancePath() } bool InstanceList::commitStagedInstance(const QString& path, - InstanceName const& instanceName, + const InstanceName& instanceName, QString groupName, - InstanceTask const& commiting) + const InstanceTask& commiting) { if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); QString instID; - InstancePtr inst; auto should_override = commiting.shouldOverride(); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index fc4fa9a39..f0a92d273 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -50,7 +50,7 @@ struct InstanceName; using InstanceId = QString; using GroupId = QString; -using InstanceLocator = std::pair; +using InstanceLocator = std::pair; enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateError, InstExists, CantCreateDir }; @@ -73,7 +73,7 @@ class InstanceList : public QAbstractListModel { Q_OBJECT public: - explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); + explicit InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: @@ -96,17 +96,17 @@ class InstanceList : public QAbstractListModel { */ enum InstListError { NoError = 0, UnknownError }; - InstancePtr at(int i) const { return m_instances.at(i); } + BaseInstance* at(int i) const { return m_instances.at(i).get(); } - int count() const { return m_instances.count(); } + int count() const { return static_cast(m_instances.size()); } InstListError loadList(); void saveNow(); /* O(n) */ - InstancePtr getInstanceById(QString id) const; + BaseInstance* getInstanceById(QString id) const; /* O(n) */ - InstancePtr getInstanceByManagedName(const QString& managed_name) const; + BaseInstance* getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString& id) const; QStringList getGroups(); bool isGroupCollapsed(const QString& groupName); @@ -179,11 +179,11 @@ class InstanceList : public QAbstractListModel { void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); - void add(const QList& list); + void add(std::vector>& list); void loadGroupList(); void saveGroupList(); QList discoverInstances(); - InstancePtr loadInstance(const InstanceId& id); + std::unique_ptr loadInstance(const InstanceId& id); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); @@ -192,11 +192,11 @@ class InstanceList : public QAbstractListModel { int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; - QList m_instances; + std::vector> m_instances; // id -> refs QMap m_groupNameCache; - SettingsObjectPtr m_globalSettings; + SettingsObject* m_globalSettings; QString m_instDir; QFileSystemWatcher* m_watcher; // FIXME: this is so inefficient that looking at it is almost painful. diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index c683774d2..134fb8f24 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -21,26 +21,26 @@ class InstancePageProvider : protected QObject, public BasePageProvider { Q_OBJECT public: - explicit InstancePageProvider(InstancePtr parent) { inst = parent; } + explicit InstancePageProvider(BaseInstance* parent) { inst = parent; } virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; values.append(new LogPage(inst)); - std::shared_ptr onesix = std::dynamic_pointer_cast(inst); - values.append(new VersionPage(onesix.get())); - values.append(ManagedPackPage::createPage(onesix.get())); - auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); + MinecraftInstance* onesix = dynamic_cast(inst); + values.append(new VersionPage(onesix)); + values.append(ManagedPackPage::createPage(onesix)); + auto modsPage = new ModFolderPage(onesix, onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); values.append(modsPage); - values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); - values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); - values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); - values.append(new GlobalDataPackPage(onesix.get())); - values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); - values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); - values.append(new NotesPage(onesix.get())); + values.append(new CoreModFolderPage(onesix, onesix->coreModList())); + values.append(new NilModFolderPage(onesix, onesix->nilModList())); + values.append(new ResourcePackPage(onesix, onesix->resourcePackList())); + values.append(new GlobalDataPackPage(onesix)); + values.append(new TexturePackPage(onesix, onesix->texturePackList())); + values.append(new ShaderPackPage(onesix, onesix->shaderPackList())); + values.append(new NotesPage(onesix)); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); @@ -52,5 +52,5 @@ class InstancePageProvider : protected QObject, public BasePageProvider { virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: - InstancePtr inst; + BaseInstance* inst; }; diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index be10bbe07..01998a7aa 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -1,4 +1,5 @@ #include "InstanceTask.h" +#include #include "Application.h" #include "settings/SettingsObject.h" @@ -82,3 +83,13 @@ void InstanceName::setName(InstanceName& other) } InstanceTask::InstanceTask() : Task(), InstanceName() {} + +ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent) +{ + auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Delete Existing Save Files"), + QObject::tr("An earlier version of this mod pack installed save files.\n" + "Would you like to remove those existing saves as part of this update?"), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); + auto result = dialog->exec(); + return result == QMessageBox::Yes ? ShouldDeleteSaves::Yes : ShouldDeleteSaves::No; +} diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 86b4cee68..125930a27 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -8,6 +8,8 @@ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); enum class ShouldUpdate { Update, SkipUpdating, Cancel }; [[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); +enum class ShouldDeleteSaves { NotAsked, Yes, No }; +[[nodiscard]] ShouldDeleteSaves askIfShouldDeleteSaves(QWidget* parent); struct InstanceName { public: @@ -35,7 +37,7 @@ class InstanceTask : public Task, public InstanceName { InstanceTask(); ~InstanceTask() override = default; - void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; } + void setParentSettings(SettingsObject* settings) { m_globalSettings = settings; } void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } @@ -60,7 +62,7 @@ class InstanceTask : public Task, public InstanceName { } protected: /* data */ - SettingsObjectPtr m_globalSettings; + SettingsObject* m_globalSettings; QString m_instIcon; QString m_instGroup; QString m_stagingPath; diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 688f9dae7..2d3372e2e 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -303,7 +303,7 @@ QStringList toStringList(const QString& jsonString) return {}; try { return requireIsArrayOf(doc); - } catch (Json::JsonException& e) { + } catch (Json::JsonException&) { return {}; } } diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 183afefaa..2b882338c 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -40,22 +40,18 @@ #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" +#include "net/NetUtils.h" #include "ui/InstanceWindow.h" -#include "ui/MainWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" -#include -#include #include -#include #include #include -#include -#include +#include #include "BuildConfig.h" #include "JavaCommon.h" @@ -63,7 +59,7 @@ #include "tasks/Task.h" #include "ui/dialogs/ChooseOfflineNameDialog.h" -LaunchController::LaunchController() : Task() {} +LaunchController::LaunchController() = default; void LaunchController::executeTask() { @@ -86,9 +82,17 @@ void LaunchController::decideAccount() return; } - // Find an account to use. - auto accounts = APPLICATION->accounts(); - if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) { + // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used + auto* accounts = APPLICATION->accounts(); + const auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); + const auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); + if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { + m_accountToUse = accounts->defaultAccount(); + } else { + m_accountToUse = accounts->at(instanceAccountIndex); + } + + if (!accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " @@ -106,16 +110,7 @@ void LaunchController::decideAccount() } } - // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used - auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); - auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); - if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { - m_accountToUse = accounts->defaultAccount(); - } else { - m_accountToUse = accounts->at(instanceAccountIndex); - } - - if (!m_accountToUse) { + if (!m_accountToUse && accounts->anyAccountIsValid()) { // If no default account is set, ask the user which one to use. ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); @@ -132,46 +127,146 @@ void LaunchController::decideAccount() } } -bool LaunchController::askPlayDemo() +LaunchDecision LaunchController::decideLaunchMode() +{ + if (!m_accountToUse || m_wantedLaunchMode == LaunchMode::Demo) { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + const auto* accounts = APPLICATION->accounts(); + MinecraftAccountPtr accountToCheck = nullptr; + + if (m_accountToUse->accountType() != AccountType::Offline) { + accountToCheck = m_accountToUse->ownsMinecraft() ? m_accountToUse : nullptr; + } else if (const auto defaultAccount = accounts->defaultAccount(); defaultAccount && defaultAccount->ownsMinecraft()) { + accountToCheck = defaultAccount; + } else { + for (int i = 0; i < accounts->count(); i++) { + if (const auto account = accounts->at(i); account->ownsMinecraft()) { + accountToCheck = account; + break; + } + } + } + + if (!accountToCheck) { + m_actualLaunchMode = LaunchMode::Demo; + return LaunchDecision::Continue; + } + + auto state = accountToCheck->accountState(); + const bool needsRefresh = + m_wantedLaunchMode == LaunchMode::Normal && (state == AccountState::Offline || accountToCheck->shouldRefresh()); + if (state == AccountState::Unchecked || state == AccountState::Errored || needsRefresh) { + accountToCheck->refresh(); + state = AccountState::Working; + } + + if (state == AccountState::Working) { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + + // TODO: this relies on tasks' synchronous signal dispatching nature + // TODO: meaning currentTask can't complete and become null while this code is running + // TODO: this code will produce a race condition when tasks become fully async + auto task = accountToCheck->currentTask(); + progDialog.execWithTask(task.get()); + + if (task->getState() == State::AbortedByUser) { + return LaunchDecision::Abort; + } + + state = accountToCheck->accountState(); + } + + QString reauthReason; + switch (state) { + case AccountState::Errored: + reauthReason = tr("An error occurred while refreshing '%1'").arg(accountToCheck->profileName()); + break; + case AccountState::Expired: + reauthReason = tr("'%1' has expired and needs to be reauthenticated").arg(accountToCheck->profileName()); + break; + case AccountState::Disabled: + reauthReason = tr("The launcher's client identification has changed"); + break; + case AccountState::Gone: + reauthReason = tr("'%1' no longer exists on the servers").arg(accountToCheck->profileName()); + break; + default: + m_actualLaunchMode = + state == AccountState::Online && m_wantedLaunchMode == LaunchMode::Normal ? LaunchMode::Normal : LaunchMode::Offline; + return LaunchDecision::Continue; // All good to go + } + + if (reauthenticateAccount(accountToCheck, reauthReason)) { + return LaunchDecision::Undecided; + } + + return LaunchDecision::Abort; +} + +bool LaunchController::askPlayDemo() const { QMessageBox box(m_parentWidget); box.setWindowTitle(tr("Play demo?")); - box.setText( - tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " - "the demo?")); + QString text = m_accountToUse + ? tr("This account does not own Minecraft.\nYou need to purchase the game first to play the full version.") + : tr("No account was selected for launch."); + text += tr("\n\nDo you want to play the demo?"); + box.setText(text); box.setIcon(QMessageBox::Warning); - auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); - auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + const auto* demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto* cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); box.setDefaultButton(cancelButton); box.exec(); return box.clickedButton() == demoButton; } -QString LaunchController::askOfflineName(QString playerName, bool demo, bool* ok) +QString LaunchController::askOfflineName(const QString& playerName, bool* ok) { if (ok != nullptr) { *ok = false; } - // we ask the user for a player name - QString message = tr("Choose your offline mode player name."); - if (demo) { - message = tr("Choose your demo mode player name."); + QString title, message; + title = tr("Player name"); + switch (m_actualLaunchMode) { + case LaunchMode::Normal: + Q_ASSERT(false); + return ""; + case LaunchMode::Demo: + message = tr("Choose your demo mode player name"); + break; + case LaunchMode::Offline: + if (m_wantedLaunchMode == LaunchMode::Normal) { + auto netErr = m_accountToUse->accountData()->networkError; + if (Net::isServerError(netErr)) { + title = tr("Auth servers offline"); + message = tr("The Minecraft authentication servers are currently unavailable, launching in offline mode.\n\n"); + } else { + title = tr("No internet connection"); + message = tr("You are not connected to the Internet, launching in offline mode.\n\n"); + } + } + message += tr("Choose your offline mode player name"); + break; } - QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + const QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; ChooseOfflineNameDialog dialog(message, m_parentWidget); - dialog.setWindowTitle(tr("Player name")); + dialog.setWindowTitle(title); dialog.setUsername(usedname); if (dialog.exec() != QDialog::Accepted) { return {}; } - const QString name = dialog.getUsername(); - usedname = name; + usedname = dialog.getUsername(); APPLICATION->settings()->set("LastOfflinePlayerName", usedname); if (ok != nullptr) { @@ -184,192 +279,79 @@ void LaunchController::login() { decideAccount(); - if (!m_accountToUse) { - // if no account is selected, ask about demo - if (!m_demo) { - m_demo = askPlayDemo(); - } - if (m_demo) { - // we ask the user for a player name + LaunchDecision decision = decideLaunchMode(); + while (decision == LaunchDecision::Undecided) { + decision = decideLaunchMode(); + } + if (decision == LaunchDecision::Abort) { + emitAborted(); + return; + } + + if (m_actualLaunchMode == LaunchMode::Demo) { + if (m_wantedLaunchMode == LaunchMode::Demo || askPlayDemo()) { bool ok = false; - auto name = askOfflineName("Player", m_demo, &ok); + auto name = askOfflineName("Player", &ok); if (ok) { m_session = std::make_shared(); - static const QRegularExpression s_removeChars("[{}-]"); - m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString(QUuid::Id128)); launchInstance(); return; } } - // if no account is selected, we bail - emitFailed(tr("No account selected for launch.")); + + emitFailed(tr("No account selected for launch")); return; } - // we loop until the user succeeds in logging in or gives up - bool tryagain = true; - unsigned int tries = 0; + m_session = std::make_shared(); + m_session->launchMode = m_actualLaunchMode; + m_accountToUse->fillSession(m_session); - if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || - m_accountToUse->shouldRefresh()) { - // Force account refresh on the account used to launch the instance updating the AccountState - // only on first try and if it is not meant to be offline - m_accountToUse->refresh(); - } - while (tryagain) { - if (tries > 0 && tries % 3 == 0) { - auto result = - QMessageBox::question(m_parentWidget, tr("Continue launch?"), - tr("It looks like we couldn't launch after %1 tries. Usually this can be fixed by logging out and " - "logging back in your Microsoft account. If that doesn't work, Minecraft authentication servers " - "may be having an outage or you may need a VPN in your region. Do you want to continue trying?") - .arg(tries)); - - if (result == QMessageBox::No) { + if (m_accountToUse->accountType() != AccountType::Offline) { + if (m_actualLaunchMode == LaunchMode::Normal && !m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + if (ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); dialog.exec() != QDialog::Accepted) { emitAborted(); return; } } - tries++; - m_session = std::make_shared(); - m_session->wants_online = m_online; - m_session->demo = m_demo; - m_accountToUse->fillSession(m_session); - MinecraftAccountPtr accountToCheck; - - if (m_accountToUse->ownsMinecraft()) - accountToCheck = m_accountToUse; - else if (const MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); - defaultAccount != nullptr && defaultAccount->ownsMinecraft()) { - accountToCheck = defaultAccount; - } else { - for (int i = 0; i < APPLICATION->accounts()->count(); i++) { - MinecraftAccountPtr account = APPLICATION->accounts()->at(i); - if (account->ownsMinecraft()) - accountToCheck = account; - } - } - - if (accountToCheck == nullptr) { - if (!m_session->demo) - m_session->demo = askPlayDemo(); - - if (m_session->demo) - launchInstance(); - else - emitFailed(tr("Launch cancelled - account does not own Minecraft.")); - - return; - } - - switch (accountToCheck->accountState()) { - case AccountState::Offline: { - m_session->wants_online = false; - } - /* fallthrough */ - case AccountState::Online: { - if (!m_session->wants_online && m_accountToUse->accountType() != AccountType::Offline) { - // we ask the user for a player name - bool ok = false; - QString name; - if (m_offlineName.isEmpty()) { - name = askOfflineName(m_session->player_name, m_session->demo, &ok); - if (!ok) { - tryagain = false; - break; - } - } else { - name = m_offlineName; - } - m_session->MakeOffline(name); - // offline flavored game from here :3 - } else if (m_accountToUse == accountToCheck && !m_accountToUse->hasProfile()) { - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) { - tryagain = true; - continue; - } else { - emitFailed(tr("Received undetermined session status during login.")); - return; - } + if (m_actualLaunchMode == LaunchMode::Offline && m_accountToUse->accountType() != AccountType::Offline) { + bool ok = false; + QString name = m_offlineName; + if (name.isEmpty()) { + name = askOfflineName(m_session->player_name, &ok); + if (!ok) { + emitAborted(); + return; } - - if (m_accountToUse->accountType() == AccountType::Offline) - m_session->wants_online = false; - - // we own Minecraft, there is a profile, it's all ready to go! - launchInstance(); - return; - } - case AccountState::Errored: - // This means some sort of soft error that we can fix with a refresh ... so let's refresh. - case AccountState::Unchecked: { - accountToCheck->refresh(); - } - /* fallthrough */ - case AccountState::Working: { - // refresh is in progress, we need to wait for it to finish to proceed. - ProgressDialog progDialog(m_parentWidget); - progDialog.setSkipButton(true, tr("Abort")); - - auto task = accountToCheck->currentTask(); - progDialog.execWithTask(task.get()); - - // don't retry if aborted - if (task->getState() == Task::State::AbortedByUser) - tryagain = false; - - continue; - } - case AccountState::Expired: { - if (reauthenticateAccount(accountToCheck)) - continue; - return; - } - case AccountState::Disabled: { - auto errorString = tr("The launcher's client identification has changed. Please remove '%1' and try again.") - .arg(accountToCheck->profileName()); - - QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok); - emitFailed(errorString); - return; - } - case AccountState::Gone: { - auto errorString = - tr("'%1' no longer exists on the servers. It may have been migrated, in which case please add the new account " - "you migrated this one to.") - .arg(accountToCheck->profileName()); - QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok); - emitFailed(errorString); - return; } + m_session->MakeOffline(name); } } - emitFailed(tr("Failed to launch.")); + + launchInstance(); } -bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) +bool LaunchController::reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason) { auto button = QMessageBox::warning( - m_parentWidget, tr("Account refresh failed"), - tr("'%1' has expired and needs to be reauthenticated. Do you want to reauthenticate this account?").arg(account->profileName()), + m_parentWidget, tr("Account refresh failed"), tr("%1. Do you want to reauthenticate this account?").arg(reason), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); if (button == QMessageBox::StandardButton::Yes) { - auto accounts = APPLICATION->accounts(); - bool isDefault = accounts->defaultAccount() == account; - accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); + auto* accounts = APPLICATION->accounts(); + const bool isDefault = accounts->defaultAccount() == account; if (account->accountType() == AccountType::MSA) { auto newAccount = MSALoginDialog::newAccount(m_parentWidget); if (newAccount != nullptr) { + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); accounts->addAccount(newAccount); - if (isDefault) + if (isDefault) { accounts->setDefaultAccount(newAccount); + } if (m_accountToUse == account) { m_accountToUse = nullptr; @@ -380,14 +362,13 @@ bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) } } - emitFailed(tr("The account has expired and needs to be reauthenticated")); return false; } void LaunchController::launchInstance() { - Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); - Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); + Q_ASSERT(m_instance != nullptr); + Q_ASSERT(m_session.get() != nullptr); if (!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); @@ -401,37 +382,36 @@ void LaunchController::launchInstance() return; } - auto console = qobject_cast(m_parentWidget); - auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + const auto* console = qobject_cast(m_parentWidget); + const auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); if (!console && showConsole) { APPLICATION->showInstanceWindow(m_instance); } - connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); - connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); - connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); - connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); + connect(m_launcher, &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); + connect(m_launcher, &LaunchTask::succeeded, this, &LaunchController::onSucceeded); + connect(m_launcher, &LaunchTask::failed, this, &LaunchController::onFailed); + connect(m_launcher, &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); // Prepend Online and Auth Status QString online_mode; - if (m_session->wants_online) { + if (m_actualLaunchMode == LaunchMode::Normal) { online_mode = "online"; // Prepend Server Status - QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; + const QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; - m_launcher->prependStep(makeShared(m_launcher.get(), servers)); + m_launcher->prependStep(makeShared(m_launcher, servers)); } else { - online_mode = m_demo ? "demo" : "offline"; + online_mode = m_actualLaunchMode == LaunchMode::Demo ? "demo" : "offline"; } - m_launcher->prependStep( - makeShared(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher, "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version { auto versionString = QString("%1 version: %2 (%3)") .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM); - m_launcher->prependStep(makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); + m_launcher->prependStep(makeShared(m_launcher, versionString + "\n", MessageLevel::Launcher)); } m_launcher->start(); } @@ -488,10 +468,10 @@ void LaunchController::onFailed(QString reason) if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { APPLICATION->showInstanceWindow(m_instance, "console"); } - emitFailed(reason); + emitFailed(std::move(reason)); } -void LaunchController::onProgressRequested(Task* task) +void LaunchController::onProgressRequested(Task* task) const { ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 50b72eb18..bc0d14e0f 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -36,30 +36,30 @@ #pragma once #include #include -#include #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/launch/MinecraftTarget.h" class InstanceWindow; + +enum class LaunchDecision { Undecided, Continue, Abort }; + class LaunchController : public Task { Q_OBJECT public: void executeTask() override; LaunchController(); - virtual ~LaunchController() = default; + ~LaunchController() override = default; - void setInstance(InstancePtr instance) { m_instance = instance; } + void setInstance(BaseInstance* instance) { m_instance = instance; } - InstancePtr instance() { return m_instance; } + BaseInstance* instance() const { return m_instance; } - void setOnline(bool online) { m_online = online; } + void setLaunchMode(const LaunchMode mode) { m_wantedLaunchMode = mode; } void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } - void setDemo(bool demo) { m_demo = demo; } - void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } void setParentWidget(QWidget* widget) { m_parentWidget = widget; } @@ -68,7 +68,7 @@ class LaunchController : public Task { void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } - QString id() { return m_instance->id(); } + QString id() const { return m_instance->id(); } bool abort() override; @@ -76,27 +76,28 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); - bool askPlayDemo(); - QString askOfflineName(QString playerName, bool demo, bool* ok = nullptr); - bool reauthenticateAccount(MinecraftAccountPtr account); + LaunchDecision decideLaunchMode(); + bool askPlayDemo() const; + QString askOfflineName(const QString& playerName, bool* ok = nullptr); + bool reauthenticateAccount(const MinecraftAccountPtr& account, const QString& reason); private slots: void readyForLaunch(); void onSucceeded(); void onFailed(QString reason); - void onProgressRequested(Task* task); + void onProgressRequested(Task* task) const; private: + LaunchMode m_wantedLaunchMode = LaunchMode::Normal; + LaunchMode m_actualLaunchMode = LaunchMode::Normal; BaseProfilerFactory* m_profiler = nullptr; - bool m_online = true; QString m_offlineName; - bool m_demo = false; - InstancePtr m_instance; + BaseInstance* m_instance = nullptr; QWidget* m_parentWidget = nullptr; InstanceWindow* m_console = nullptr; MinecraftAccountPtr m_accountToUse = nullptr; - AuthSessionPtr m_session; - shared_qobject_ptr m_launcher; - MinecraftTarget::Ptr m_targetToJoin; + AuthSessionPtr m_session = nullptr; + LaunchTask* m_launcher = nullptr; + MinecraftTarget::Ptr m_targetToJoin = nullptr; }; diff --git a/launcher/LaunchMode.h b/launcher/LaunchMode.h new file mode 100644 index 000000000..45cfe50ce --- /dev/null +++ b/launcher/LaunchMode.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Octol1ttle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +enum class LaunchMode { + Normal, + Offline, + Demo, +}; diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 0e84bdd9d..28ba32bf8 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -15,12 +15,18 @@ fi LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ +LAUNCHER_ENVNAME=@Launcher_ENVName@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" # Makes the launcher use portals for file picking export QT_QPA_PLATFORMTHEME=xdgdesktopportal +# disable OpenGL and Vulkan launcher features on sharun until https://github.com/VHSgunzo/sharun/issues/35 +if [[ -f "${LAUNCHER_DIR}/sharun" ]]; then + export ${LAUNCHER_ENVNAME}_DISABLE_GLVULKAN=1 +fi + # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" diff --git a/launcher/MangoHud.cpp b/launcher/LibraryUtils.cpp similarity index 94% rename from launcher/MangoHud.cpp rename to launcher/LibraryUtils.cpp index d85100207..4ac038114 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/LibraryUtils.cpp @@ -25,7 +25,7 @@ #include "FileSystem.h" #include "Json.h" -#include "MangoHud.h" +#include "LibraryUtils.h" #ifdef __GLIBC__ #ifndef _GNU_SOURCE @@ -36,9 +36,9 @@ #include #endif -namespace MangoHud { +namespace LibraryUtils { -QString getLibraryString() +QString findMangoHud() { /** * Guess MangoHud install location by searching for vulkan layers in this order: @@ -123,7 +123,7 @@ QString getLibraryString() #ifdef __GLIBC__ // Check whether mangohud is usable on a glibc based system - QString libraryPath = findLibrary(libraryName); + QString libraryPath = find(libraryName); if (!libraryPath.isEmpty()) { return libraryPath; } @@ -138,7 +138,7 @@ QString getLibraryString() return {}; } -QString findLibrary(QString libName) +QString find(QString libName) { #ifdef __GLIBC__ const char* library = libName.toLocal8Bit().constData(); @@ -161,11 +161,11 @@ QString findLibrary(QString libName) dlclose(handle); return fullPath; #else - qWarning() << "MangoHud::findLibrary is not implemented on this platform"; + qWarning() << "LibraryUtils::find is not implemented on this platform"; return {}; #endif } -} // namespace MangoHud +} // namespace LibraryUtils #ifdef UNDEF_GNU_SOURCE #undef _GNU_SOURCE diff --git a/launcher/MangoHud.h b/launcher/LibraryUtils.h similarity index 87% rename from launcher/MangoHud.h rename to launcher/LibraryUtils.h index 5361999b4..6832a9627 100644 --- a/launcher/MangoHud.h +++ b/launcher/LibraryUtils.h @@ -21,9 +21,9 @@ #include #include -namespace MangoHud { +namespace LibraryUtils { -QString getLibraryString(); +QString findMangoHud(); -QString findLibrary(QString libName); -} // namespace MangoHud +QString find(QString libName); +} // namespace LibraryUtils diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index bae45ad88..c5ef47d7b 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -114,7 +114,7 @@ void LoggedProcess::on_error(QProcess::ProcessError error) { switch (error) { case QProcess::FailedToStart: { - emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); + emit log({ tr("The process failed to start: %1").arg(errorString()) }, MessageLevel::Fatal); changeState(LoggedProcess::FailedToStart); break; } diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 5e962d12a..64be89643 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -233,7 +233,7 @@ std::optional extractSubDir(ArchiveReader* zip, const QString& subd << target; return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; return false; } diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index 0ba9c5ac8..97db598de 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -14,26 +14,26 @@ else \ type = Qt::DirectConnection; -#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE, RET_DEF) \ static RET_TYPE NAME() \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ return ret; \ } -#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ return ret; \ } -#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, RET_DEF, PARAM_1_TYPE, PARAM_2_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ { \ - RET_TYPE ret; \ + RET_TYPE ret = RET_DEF; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ Q_ARG(PARAM_2_TYPE, p2)); \ @@ -53,18 +53,18 @@ class PixmapCache final : public QObject { static void setInstance(PixmapCache* i) { s_instance = i; } public: - DEFINE_FUNC_NO_PARAM(cacheLimit, int) - DEFINE_FUNC_NO_PARAM(clear, bool) - DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) - DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) - DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) - DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) - DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) - DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) - DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) - DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) + DEFINE_FUNC_NO_PARAM(cacheLimit, int, -1) + DEFINE_FUNC_NO_PARAM(clear, bool, false) + DEFINE_FUNC_TWO_PARAM(find, bool, false, const QString&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(find, bool, false, const QPixmapCache::Key&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(insert, bool, false, const QString&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, {}, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QString&) + DEFINE_FUNC_ONE_PARAM(remove, bool, false, const QPixmapCache::Key&) + DEFINE_FUNC_TWO_PARAM(replace, bool, false, const QPixmapCache::Key&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, false, int) + DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool, false) + DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, false, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 13ad0b2c5..1c2425e16 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -41,8 +41,8 @@ class NullInstance : public BaseInstance { Q_OBJECT public: - NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) - : BaseInstance(globalSettings, settings, rootDir) + NullInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) + : BaseInstance(globalSettings, std::move(settings), rootDir) { setVersionBroken(true); } @@ -52,7 +52,7 @@ class NullInstance : public BaseInstance { QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; - shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } + LaunchTask* createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h index 67a57a1a2..533195e94 100644 --- a/launcher/PSaveFile.h +++ b/launcher/PSaveFile.h @@ -67,5 +67,5 @@ class PSaveFile : public QSaveFile { QString m_absoluteFilePath; }; #else -#define PSaveFile QSaveFile +using PSaveFile = QSaveFile; #endif diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 2919ddd19..bb44d2495 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -19,23 +19,52 @@ #include "ResourceDownloadTask.h" +#include + #include "Application.h" #include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" +namespace { +Net::ModrinthDownloadMeta createModrinthMeta(BaseInstance* instance, QString reason) +{ + auto* mcInstance = dynamic_cast(instance); + if (!mcInstance) { + return {}; + } + + auto* profile = mcInstance->getPackProfile(); + if (!profile) { + return {}; + } + + auto loaders = profile->getModLoadersList(); + + return { + .reason = std::move(reason), + .gameVersion = profile->getComponentVersion("net.minecraft"), + .loader = !loaders.isEmpty() ? ModPlatform::getModLoaderAsString(loaders.first()) : "", + }; +} +} // namespace + ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - const std::shared_ptr packs, - bool is_indexed) + ResourceFolderModel* packs, + bool isIndexed, + QString downloadReason) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) { - if (is_indexed) { + if (isIndexed) { m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); @@ -45,7 +74,9 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); - auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())); + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename()), + Net::Download::Option::NoOptions, + createModrinthMeta(m_pack_model->instance(), std::move(downloadReason))); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: @@ -82,21 +113,23 @@ void ResourceDownloadTask::downloadSucceeded() auto oldName = std::get<0>(to_delete); auto oldFilename = std::get<1>(to_delete); - if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) + if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) { return; + } m_pack_model->uninstallResource(oldFilename, true); // also rename the shader config file - if (dynamic_cast(m_pack_model.get()) != nullptr) { + if (dynamic_cast(m_pack_model) != nullptr) { QFileInfo oldConfig(m_pack_model->dir(), oldFilename + ".txt"); QFileInfo newConfig(m_pack_model->dir(), getFilename() + ".txt"); if (oldConfig.exists() && !newConfig.exists()) { bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); - if (!success) + if (!success) { emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); + } } } } @@ -104,7 +137,7 @@ void ResourceDownloadTask::downloadSucceeded() void ResourceDownloadTask::downloadFailed(QString reason) { m_filesNetJob.reset(); - emitFailed(reason); + emitFailed(std::move(reason)); } void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) @@ -114,7 +147,7 @@ void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) // This indirection is done so that we don't delete a mod before being sure it was // downloaded successfully! -void ResourceDownloadTask::hasOldResource(QString name, QString filename) +void ResourceDownloadTask::hasOldResource(const QString& name, const QString& filename) { to_delete = { name, filename }; } diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index 78fe8efc7..84324e99a 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -32,8 +32,9 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - std::shared_ptr packs, - bool is_indexed = true); + ResourceFolderModel* packs, + bool isIndexed = true, + QString downloadReason = "standalone"); const QString& getFilename() const { return m_pack_version.fileName; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } @@ -44,7 +45,7 @@ class ResourceDownloadTask : public SequentialTask { private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; - const std::shared_ptr m_pack_model; + ResourceFolderModel* m_pack_model; NetJob::Ptr m_filesNetJob; LocalResourceUpdateTask::Ptr m_update_task; @@ -56,5 +57,5 @@ class ResourceDownloadTask : public SequentialTask { std::tuple to_delete{ "", "" }; private slots: - void hasOldResource(QString name, QString filename); + void hasOldResource(const QString& name, const QString& filename); }; diff --git a/launcher/RuntimeContext.h b/launcher/RuntimeContext.h index 85304a5bc..84a56a892 100644 --- a/launcher/RuntimeContext.h +++ b/launcher/RuntimeContext.h @@ -41,7 +41,7 @@ struct RuntimeContext { return javaRealArchitecture; } - void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) + void updateFromInstanceSettings(SettingsObject* instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp index cfcf63805..d02b1d854 100644 --- a/launcher/SysInfo.cpp +++ b/launcher/SysInfo.cpp @@ -1,20 +1,54 @@ -#include + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 r58Playz + * Copyright (C) 2024 timoreo + * Copyright (C) 2024 Trial97 + * Copyright (C) 2025 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include -#include "sys.h" -#ifdef Q_OS_MACOS -#include -#endif -#include -#include -#include -#include + +#include "HardwareInfo.h" #ifdef Q_OS_MACOS +#include + bool rosettaDetect() { int ret = 0; size_t size = sizeof(ret); - if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { return false; } return ret == 1; @@ -51,18 +85,13 @@ QString useQTForArch() return QSysInfo::currentCpuArchitecture(); } -int suitableMaxMem() +int defaultMaxJvmMem() { - float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; - int maxMemoryAlloc; - // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB - if (totalRAM < (4096 * 1.5)) - maxMemoryAlloc = (int)(totalRAM / 1.5); + if (const uint64_t totalRAM = HardwareInfo::totalRamMiB(); totalRAM < (4096 * 1.5)) + return totalRAM / 1.5; else - maxMemoryAlloc = 4096; - - return maxMemoryAlloc; + return 4096; } QString getSupportedJavaArchitecture() diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h index f6c04d702..da23c5be1 100644 --- a/launcher/SysInfo.h +++ b/launcher/SysInfo.h @@ -1,9 +1,12 @@ #pragma once + +#include + #include namespace SysInfo { QString currentSystem(); QString useQTForArch(); QString getSupportedJavaArchitecture(); -int suitableMaxMem(); +int defaultMaxJvmMem(); } // namespace SysInfo diff --git a/launcher/Usable.h b/launcher/Usable.h index b0ecd4018..8cef29868 100644 --- a/launcher/Usable.h +++ b/launcher/Usable.h @@ -36,7 +36,7 @@ class Usable { */ class UseLock { public: - UseLock(shared_qobject_ptr usable) : m_usable(usable) + UseLock(Usable* usable) : m_usable(usable) { // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. m_usable->incrementUses(); @@ -44,5 +44,5 @@ class UseLock { ~UseLock() { m_usable->decrementUses(); } private: - shared_qobject_ptr m_usable; + Usable* m_usable; }; diff --git a/launcher/Version.cpp b/launcher/Version.cpp index bffe5d58a..d5496ce1c 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,124 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ #include "Version.h" #include #include #include - -Version::Version(QString str) : m_string(std::move(str)) -{ - parse(); -} - -#define VERSION_OPERATOR(return_on_different) \ - bool exclude_our_sections = false; \ - bool exclude_their_sections = false; \ - \ - const auto size = qMax(m_sections.size(), other.m_sections.size()); \ - for (int i = 0; i < size; ++i) { \ - Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ - Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ - \ - { /* Don't include appendixes in the comparison */ \ - if (sec1.isAppendix()) \ - exclude_our_sections = true; \ - if (sec2.isAppendix()) \ - exclude_their_sections = true; \ - \ - if (exclude_our_sections) { \ - sec1 = Section(); \ - if (sec2.m_isNull) \ - break; \ - } \ - \ - if (exclude_their_sections) { \ - sec2 = Section(); \ - if (sec1.m_isNull) \ - break; \ - } \ - } \ - \ - if (sec1 != sec2) \ - return return_on_different; \ - } - -bool Version::operator<(const Version& other) const -{ - VERSION_OPERATOR(sec1 < sec2) - - return false; -} -bool Version::operator==(const Version& other) const -{ - VERSION_OPERATOR(false) - - return true; -} -bool Version::operator!=(const Version& other) const -{ - return !operator==(other); -} -bool Version::operator<=(const Version& other) const -{ - return *this < other || *this == other; -} -bool Version::operator>(const Version& other) const -{ - return !(*this <= other); -} -bool Version::operator>=(const Version& other) const -{ - return !(*this < other); -} - -void Version::parse() -{ - m_sections.clear(); - QString currentSection; - - if (m_string.isEmpty()) - return; - - auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { - if (lastChar.isNull()) - return false; - if (lastChar.isDigit() != currentChar.isDigit()) - return true; - - const QList s_separators{ '.', '-', '+' }; - if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) - return true; - - return false; - }; - - currentSection += m_string.at(0); - for (int i = 1; i < m_string.size(); ++i) { - const auto& current_char = m_string.at(i); - if (classChange(m_string.at(i - 1), current_char)) { - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); - currentSection = ""; - } - - currentSection += current_char; - } - - if (!currentSection.isEmpty()) - m_sections.append(Section(currentSection)); -} +#include /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) { - QDebugStateSaver saver(debug); + const QDebugStateSaver saver(debug); debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; bool first = true; - for (auto s : v.m_sections) { - if (!first) + for (const auto& s : v.m_sections) { + if (!first) { debug.nospace() << ", "; - debug.nospace() << s.m_fullString; + } + debug.nospace() << s.value; first = false; } @@ -126,3 +44,114 @@ QDebug operator<<(QDebug debug, const Version& v) return debug; } + +std::strong_ordering Version::Section::operator<=>(const Section& other) const +{ + // If both components are numeric, compare numerically (codepoint-wise) + if (this->t == Type::Numeric && other.t == Type::Numeric) { + auto aLen = this->value.size(); + if (aLen != other.value.size()) { + // Lengths differ; compare by length + return aLen <=> other.value.size(); + } + // Compare by digits + auto cmp = QString::compare(this->value, other.value); + if (cmp < 0) { + return std::strong_ordering::less; + } + if (cmp > 0) { + return std::strong_ordering::greater; + } + return std::strong_ordering::equal; + } + // One or both are null + if (this->t == Type::Null) { + if (other.t == Type::PreRelease) { + return std::strong_ordering::greater; + } + return std::strong_ordering::less; + } + if (other.t == Type::Null) { + if (this->t == Type::PreRelease) { + return std::strong_ordering::less; + } + return std::strong_ordering::greater; + } + // Textual comparison (differing type, or both textual/pre-release) + auto minLen = qMin(this->value.size(), other.value.size()); + for (int i = 0; i < minLen; i++) { + auto a = this->value.at(i); + auto b = other.value.at(i); + if (a != b) { + // Compare by rune + return a.unicode() <=> b.unicode(); + } + } + // Compare by length + return this->value.size() <=> other.value.size(); +} + +namespace { +void removeLeadingZeros(QString& s) +{ + s.remove(0, std::distance(s.begin(), std::ranges::find_if_not(s, [](QChar c) { return c == '0'; }))); +} +} // namespace + +void Version::parse() +{ + auto len = m_string.size(); + for (int i = 0; i < len;) { + Section cur(Section::Type::Textual); + auto c = m_string.at(i); + if (c == '+') { + break; // Ignore appendices + } + // custom: the space is special to handle the strings like "1.20 Pre-Release 1" + // this is needed to support Modrinth versions + if (c == '-' || c == ' ') { + // Add dash to component + cur.value += c; + i++; + // If the next rune is non-digit, mark as pre-release (requires >= 1 non-digit after dash so the component has length > 1) + if (i < len && !m_string.at(i).isDigit()) { + cur.t = Section::Type::PreRelease; + } + } else if (c.isDigit()) { + // Mark as numeric + cur.t = Section::Type::Numeric; + } + for (; i < len; i++) { + auto r = m_string.at(i); + if ((r.isDigit() != (cur.t == Section::Type::Numeric)) // starts a new section + || (r == ' ' && cur.t == Section::Type::Numeric) // custom: numeric section then a space is a pre-release + || (r == '-' && cur.t != Section::Type::PreRelease) // "---" is a valid pre-release component + || r == '+') { + // Run completed (do not consume this rune) + break; + } + // Add rune to current run + cur.value += r; + } + if (!cur.value.isEmpty()) { + if (cur.t == Section::Type::Numeric) { + removeLeadingZeros(cur.value); + } + m_sections.append(cur); + } + } +} + +std::strong_ordering Version::operator<=>(const Version& other) const +{ + const auto size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) { + auto sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); + auto sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); + + if (auto cmp = sec1 <=> sec2; cmp != std::strong_ordering::equal) { + return cmp; + } + } + return std::strong_ordering::equal; +} diff --git a/launcher/Version.h b/launcher/Version.h index 4b5ea7119..c0f70f487 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2026 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,23 +16,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ #pragma once @@ -41,115 +25,36 @@ #include #include -class QUrl; - +// this implements the FlexVer +// https://git.sleeping.town/exa/FlexVer class Version { public: - Version(QString str); + Version(QString str) : m_string(std::move(str)) { parse(); } // NOLINT(hicpp-explicit-conversions) Version() = default; - bool operator<(const Version& other) const; - bool operator<=(const Version& other) const; - bool operator>(const Version& other) const; - bool operator>=(const Version& other) const; - bool operator==(const Version& other) const; - bool operator!=(const Version& other) const; + private: + struct Section { + enum class Type : std::uint8_t { Null, Textual, Numeric, PreRelease }; + explicit Section(Type t = Type::Null, QString value = "") : t(t), value(std::move(value)) {} + Type t; + QString value; + bool operator==(const Section& other) const = default; + std::strong_ordering operator<=>(const Section& other) const; + }; + private: + void parse(); + + public: QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); - private: - struct Section { - explicit Section(QString fullString) : m_fullString(std::move(fullString)) - { - qsizetype cutoff = m_fullString.size(); - for (int i = 0; i < m_fullString.size(); i++) { - if (!m_fullString[i].isDigit()) { - cutoff = i; - break; - } - } - - auto numPart = QStringView{ m_fullString }.left(cutoff); - - if (!numPart.isEmpty()) { - m_isNull = false; - m_numPart = numPart.toInt(); - } - - auto stringPart = QStringView{ m_fullString }.mid(cutoff); - - if (!stringPart.isEmpty()) { - m_isNull = false; - m_stringPart = stringPart.toString(); - } - } - - explicit Section() = default; - - bool m_isNull = true; - - int m_numPart = 0; - QString m_stringPart; - - QString m_fullString; - - inline bool isAppendix() const { return m_stringPart.startsWith('+'); } - inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } - - inline bool operator==(const Section& other) const - { - if (m_isNull && !other.m_isNull) - return false; - if (!m_isNull && other.m_isNull) - return false; - - if (!m_isNull && !other.m_isNull) { - return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); - } - - return true; - } - - inline bool operator<(const Section& other) const - { - static auto unequal_is_less = [](Section const& non_null) -> bool { - if (non_null.m_stringPart.isEmpty()) - return non_null.m_numPart == 0; - return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); - }; - - if (!m_isNull && other.m_isNull) - return unequal_is_less(*this); - if (m_isNull && !other.m_isNull) - return !unequal_is_less(other); - - if (!m_isNull && !other.m_isNull) { - if (m_numPart < other.m_numPart) - return true; - if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) - return true; - - if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) - return false; - if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) - return true; - - return false; - } - - return m_fullString < other.m_fullString; - } - - inline bool operator!=(const Section& other) const { return !(*this == other); } - inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } - }; + bool operator==(const Version& other) const { return (*this <=> other) == std::strong_ordering::equal; } + std::strong_ordering operator<=>(const Version& other) const; private: QString m_string; QList
m_sections; - - void parse(); -}; +}; \ No newline at end of file diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 32048db8e..aaab7e8e0 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -198,9 +198,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const return tr("Latest"); } } - } else { - return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } case Qt::DecorationRole: { if (column == Name && hasRecommended) { diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp index 1f87d8237..764063de3 100644 --- a/launcher/archive/ArchiveReader.cpp +++ b/launcher/archive/ArchiveReader.cpp @@ -23,7 +23,10 @@ #include #include #include +#include +#include #include +#include namespace MMCZip { QStringList ArchiveReader::getFiles() @@ -34,25 +37,36 @@ QStringList ArchiveReader::getFiles() bool ArchiveReader::collectFiles(bool onlyFiles) { return parse([this, onlyFiles](File* f) { - if (!onlyFiles || f->isFile()) + if (!onlyFiles || f->isFile()) { m_fileNames << f->filename(); + } return f->skip(); }); } +using getPathFunc = std::function; +static QString decodeLibArchivePath(archive_entry* entry, const getPathFunc& getUtf8Path, const getPathFunc& getPath) +{ + auto fileName = QString::fromUtf8(getUtf8Path(entry)); + if (fileName.isEmpty()) { + fileName = QString::fromLocal8Bit(getPath(entry)); + } + return fileName; +} + QString ArchiveReader::File::filename() { - return QString::fromUtf8(archive_entry_pathname_utf8(m_entry)); + return decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname); } QByteArray ArchiveReader::File::readAll(int* outStatus) { QByteArray data; - const void* buff; - size_t size; - la_int64_t offset; + const void* buff = nullptr; + size_t size = 0; + la_int64_t offset = 0; - int status; + int status = 0; while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { data.append(static_cast(buff), static_cast(size)); } @@ -78,10 +92,10 @@ int ArchiveReader::File::readNextHeader() return archive_read_next_header(m_archive.get(), &m_entry); } -auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr +auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr { auto f = std::make_unique(); - auto a = f->m_archive.get(); + auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); @@ -103,15 +117,16 @@ auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) { - int r; - const void* buff; - size_t size; - la_int64_t offset; + int r = 0; + const void* buff = nullptr; + size_t size = 0; + la_int64_t offset = 0; for (;;) { r = archive_read_data_block(ar, &buff, &size, &offset); - if (r == ARCHIVE_EOF) - return (ARCHIVE_OK); + if (r == ARCHIVE_EOF) { + return ARCHIVE_OK; + } if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(ar); return (r); @@ -128,9 +143,43 @@ static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = fal } } -bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) +static bool willEscapeRoot(const QDir& root, archive_entry* entry) { - auto entry = m_entry; + auto entryPath = decodeLibArchivePath(entry, archive_entry_pathname_utf8, archive_entry_pathname); + auto linkTarget = decodeLibArchivePath(entry, archive_entry_symlink_utf8, archive_entry_symlink); + auto hardLink = decodeLibArchivePath(entry, archive_entry_hardlink_utf8, archive_entry_hardlink); + + if (entryPath.isEmpty() || (linkTarget.isEmpty() && hardLink.isEmpty())) { + return false; + } + + bool isHardLink = false; + if (isHardLink = linkTarget.isEmpty(); isHardLink) { + linkTarget = hardLink; + } + + QString linkFullPath = root.filePath(entryPath); + auto rootDir = QUrl::fromLocalFile(root.absolutePath()); + + if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) { + return true; + } + + QDir linkDir = QFileInfo(linkFullPath).dir(); + if (!QDir::isAbsolutePath(linkTarget)) { + linkTarget = (!isHardLink ? linkDir : root).filePath(linkTarget); + } + return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget))); +} + +bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, bool notBlock) +{ + return writeFile(out, targetFileName, {}, notBlock); +}; + +bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock) +{ + auto* entry = m_entry; std::unique_ptr entryClone(nullptr, &archive_entry_free); if (!targetFileName.isEmpty()) { entryClone.reset(archive_entry_clone(m_entry)); @@ -138,26 +187,34 @@ bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool n auto nameUtf8 = targetFileName.toUtf8(); archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); } - if (archive_write_header(out, entry) < ARCHIVE_OK) { - qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); + if (root.has_value() && willEscapeRoot(root.value(), entry)) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root"; return false; - } else if (archive_entry_size(m_entry) > 0) { + } + if (archive_write_header(out, entry) < ARCHIVE_OK) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out) << targetFileName; + return false; + } + if (archive_entry_size(m_entry) > 0) { auto r = copy_data(m_archive.get(), out, notBlock); - if (r < ARCHIVE_OK) + if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(out); - if (r < ARCHIVE_WARN) + } + if (r < ARCHIVE_WARN) { return false; + } } auto r = archive_write_finish_entry(out); - if (r < ARCHIVE_OK) + if (r < ARCHIVE_OK) { qCritical() << "Failed to finish writing entry:" << archive_error_string(out); + } return (r >= ARCHIVE_WARN); } -bool ArchiveReader::parse(std::function doStuff) +bool ArchiveReader::parse(const std::function& doStuff) { auto f = std::make_unique(); - auto a = f->m_archive.get(); + auto* a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); @@ -181,7 +238,7 @@ bool ArchiveReader::parse(std::function doStuff) return true; } -bool ArchiveReader::parse(std::function doStuff) +bool ArchiveReader::parse(const std::function& doStuff) { return parse([doStuff](File* f, bool&) { return doStuff(f); }); } @@ -205,26 +262,32 @@ QString ArchiveReader::getZipName() bool ArchiveReader::exists(const QString& filePath) const { - if (filePath == QLatin1String("/") || filePath.isEmpty()) + if (filePath == QLatin1String("/") || filePath.isEmpty()) { return true; + } // Normalize input path (remove trailing slash, if any) QString normalizedPath = QDir::cleanPath(filePath); - if (normalizedPath.startsWith('/')) + if (normalizedPath.startsWith('/')) { normalizedPath.remove(0, 1); - if (normalizedPath == QLatin1String(".")) + } + if (normalizedPath == QLatin1String(".")) { return true; - if (normalizedPath == QLatin1String("..")) + } + if (normalizedPath == QLatin1String("..")) { return false; // root only + } // Check for exact file match - if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) + if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) { return true; + } // Check for directory existence by seeing if any file starts with that path QString dirPath = normalizedPath + QLatin1Char('/'); for (const QString& f : m_fileNames) { - if (f.startsWith(dirPath, Qt::CaseInsensitive)) + if (f.startsWith(dirPath, Qt::CaseInsensitive)) { return true; + } } return false; diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h index aaeba6095..4f11d2e06 100644 --- a/launcher/archive/ArchiveReader.h +++ b/launcher/archive/ArchiveReader.h @@ -19,8 +19,11 @@ #include #include +#include #include #include +#include +#include struct archive; struct archive_entry; @@ -28,7 +31,7 @@ namespace MMCZip { class ArchiveReader { public: using ArchivePtr = std::unique_ptr; - ArchiveReader(QString fileName) : m_archivePath(fileName) {} + explicit ArchiveReader(QString fileName) : m_archivePath(std::move(fileName)) {} virtual ~ArchiveReader() = default; QStringList getFiles(); @@ -48,7 +51,8 @@ class ArchiveReader { QByteArray readAll(int* outStatus = nullptr); bool skip(); - bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); + bool writeFile(archive* out, const QString& targetFileName = "", bool notBlock = false); + bool writeFile(archive* out, const QString& targetFileName, std::optional root, bool notBlock = false); private: int readNextHeader(); @@ -59,14 +63,14 @@ class ArchiveReader { archive_entry* m_entry; }; - std::unique_ptr goToFile(QString filename); - bool parse(std::function); - bool parse(std::function); + std::unique_ptr goToFile(const QString& filename); + bool parse(const std::function&); + bool parse(const std::function&); private: QString m_archivePath; size_t m_blockSize = 10240; - QStringList m_fileNames = {}; + QStringList m_fileNames; }; } // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.cpp b/launcher/archive/ArchiveWriter.cpp index 8436e5486..43dbe4dbd 100644 --- a/launcher/archive/ArchiveWriter.cpp +++ b/launcher/archive/ArchiveWriter.cpp @@ -174,7 +174,7 @@ bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) if (fileInfo.isFile() && !fileInfo.isSymLink()) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file:" << fileInfo.filePath(); + qCritical() << "Failed to open file:" << fileInfo.filePath() << "error:" << file.errorString(); return false; } diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp index acbcd39d5..35dc39d90 100644 --- a/launcher/archive/ExtractZipTask.cpp +++ b/launcher/archive/ExtractZipTask.cpp @@ -95,7 +95,7 @@ auto ExtractZipTask::extractZip() -> ZipResult return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); return false; } diff --git a/launcher/console/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp index 4a0eb3d3d..e12183624 100644 --- a/launcher/console/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -24,12 +24,16 @@ #endif #include +#include #include +#include #include #include #include #include +namespace console { + void RedirectHandle(DWORD handle, FILE* stream, const char* mode) { HANDLE stdHandle = GetStdHandle(handle); @@ -157,3 +161,31 @@ std::error_code EnableAnsiSupport() return {}; } + +void FreeWindowsConsole() +{ + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); +} + +WindowsConsoleGuard::WindowsConsoleGuard() : m_consoleAttached(false) +{ + if (console::AttachWindowsConsole()) { + m_consoleAttached = true; + if (auto err = console::EnableAnsiSupport(); err) { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +} + +WindowsConsoleGuard::~WindowsConsoleGuard() +{ + // Detach from Windows console + if (m_consoleAttached) { + console::FreeWindowsConsole(); + } +} + +} // namespace console diff --git a/launcher/console/WindowsConsole.h b/launcher/console/WindowsConsole.h index 4c1f3ee28..52102217d 100644 --- a/launcher/console/WindowsConsole.h +++ b/launcher/console/WindowsConsole.h @@ -21,8 +21,24 @@ #pragma once -#include +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +namespace console { void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); std::error_code EnableAnsiSupport(); +void FreeWindowsConsole(); + +class WindowsConsoleGuard { + public: + WindowsConsoleGuard(); + ~WindowsConsoleGuard(); + + private: + bool m_consoleAttached; +}; + +} // namespace console diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index 343f31eaa..d87a1078b 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -34,26 +34,11 @@ #include -#include - -#if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include "console/WindowsConsole.h" -#endif - #include namespace fs = std::filesystem; FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { -#if defined Q_OS_WIN32 - // attach the parent console - if (AttachWindowsConsole()) { - consoleAttached = true; - } -#endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); @@ -236,13 +221,4 @@ FileLinkApp::~FileLinkApp() qDebug() << "link program shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); - -#if defined Q_OS_WIN32 - // Detach from Windows console - if (consoleAttached) { - fclose(stdout); - fclose(stdin); - fclose(stderr); - } -#endif } diff --git a/launcher/filelink/FileLink.h b/launcher/filelink/FileLink.h index 583d0d43a..25fdb71fc 100644 --- a/launcher/filelink/FileLink.h +++ b/launcher/filelink/FileLink.h @@ -38,7 +38,6 @@ #include "FileSystem.h" class FileLinkApp : public QCoreApplication { - // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized }; @@ -64,8 +63,4 @@ class FileLinkApp : public QCoreApplication { QList m_links_to_make; QList m_path_results; -#if defined Q_OS_WIN32 - // used on Windows to attach the standard IO streams - bool consoleAttached = false; -#endif }; diff --git a/launcher/filelink/filelink_main.cpp b/launcher/filelink/filelink_main.cpp index 2a8bcb703..d34844370 100644 --- a/launcher/filelink/filelink_main.cpp +++ b/launcher/filelink/filelink_main.cpp @@ -22,8 +22,17 @@ #include "FileLink.h" +#if defined Q_OS_WIN32 +#include "console/WindowsConsole.h" +#endif + int main(int argc, char* argv[]) { +#if defined Q_OS_WIN32 + // attach the parent console + console::WindowsConsoleGuard _consoleGuard; +#endif + FileLinkApp ldh(argc, argv); switch (ldh.status()) { diff --git a/launcher/include/base.pch.hpp b/launcher/include/base.pch.hpp index c858857f9..ecaf41fdd 100644 --- a/launcher/include/base.pch.hpp +++ b/launcher/include/base.pch.hpp @@ -3,6 +3,7 @@ #define PRISM_PRECOMPILED_BASE_HEADERS_H #include +#include #include #include #include @@ -12,6 +13,5 @@ #include #include #include -#include #endif // PRISM_PRECOMPILED_BASE_HEADERS_H diff --git a/launcher/include/qtcore.pch.hpp b/launcher/include/qtcore.pch.hpp index d7c24ddb6..b8836618a 100644 --- a/launcher/include/qtcore.pch.hpp +++ b/launcher/include/qtcore.pch.hpp @@ -5,9 +5,15 @@ #include #include #include + +#include +#include + #include +#include #include +#include #include diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 23bd1b73e..3a04756fa 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -163,7 +163,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; auto java_vendor = results["java.vendor"]; - bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64"; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64" || os_arch == "ppc64le" || os_arch == "ppc64"; result.validity = Result::Validity::Valid; result.is_64bit = is_64; @@ -179,13 +179,20 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) void JavaChecker::error(QProcess::ProcessError err) { if (err == QProcess::FailedToStart) { - qDebug() << "Java checker has failed to start."; + qDebug() << "Java checker has failed to start:" << process->errorString(); qDebug() << "Process environment:"; qDebug() << process->environment(); qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); - emit checkFinished({ m_path, m_id }); + + Result result = { + m_path, + m_id, + }; + result.errorLog = process->errorString(); + result.validity = Result::Validity::Errored; + emit checkFinished(result); } emitSucceeded(); } diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 30cb77e08..98aac5cab 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -49,7 +49,7 @@ bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } @@ -58,7 +58,7 @@ bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index d8fd477fd..5899964f0 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -39,7 +39,6 @@ struct JavaInstall : public BaseVersion { JavaVersion id; QString arch; QString path; - bool recommended = false; bool is_64bit = false; }; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index aa7fab8a0..1d7b9cdff 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -41,6 +41,7 @@ #include #include "Application.h" +#include "settings/SettingsObject.h" #include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -50,8 +51,9 @@ JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) {} -Task::Ptr JavaInstallList::getLoadTask() +Task::Ptr JavaInstallList::getLoadTask(bool forceReload) { + Q_UNUSED(forceReload) load(); return getCurrentTask(); } @@ -107,7 +109,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const case VersionRole: return version->id.toString(); case RecommendedRole: - return version->recommended; + return false; case PathRole: return version->path; case CPUArchitectureRole: @@ -127,10 +129,6 @@ void JavaInstallList::updateListData(QList versions) beginResetModel(); m_vlist = versions; sortVersions(); - if (m_vlist.size()) { - auto best = std::dynamic_pointer_cast(m_vlist[0]); - best->recommended = true; - } endResetModel(); m_status = Status::Done; m_load_task.reset(); diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index c68c2a3be..58d1ad8ca 100644 --- a/launcher/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList { public: explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); - Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask(bool forceReload = false) override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp index 115baa9e5..3647c963f 100644 --- a/launcher/java/JavaMetadata.cpp +++ b/launcher/java/JavaMetadata.cpp @@ -111,7 +111,7 @@ bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator<(a); } } @@ -120,7 +120,7 @@ bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); - } catch (const std::bad_cast& e) { + } catch (const std::bad_cast&) { return BaseVersion::operator>(a); } } diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 25fe1caa8..c58fe5601 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -42,6 +42,7 @@ #include #include "Application.h" +#include "BuildConfig.h" #include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" @@ -155,7 +156,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() QStringList addJavasFromEnv(QList javas) { - auto env = qEnvironmentVariable("PRISMLAUNCHER_JAVA_PATHS"); // FIXME: use launcher name from buildconfig + auto env = QProcessEnvironment::systemEnvironment().value(QStringLiteral("%1_JAVA_PATHS").arg(BuildConfig.LAUNCHER_ENVNAME)); #if defined(Q_OS_WIN32) QList javaPaths = env.replace("\\", "/").split(QLatin1String(";")); diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 28c6a1831..0a51741a2 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -39,9 +39,8 @@ void ManifestDownloadTask::executeTask() { setStatus(tr("Downloading Java")); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); - auto files = std::make_shared(); - auto action = Net::Download::makeByteArray(m_url, files); + auto [action, files] = Net::Download::makeByteArray(m_url); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index eb1a8b37a..26b2b582d 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -51,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) +std::unique_ptr LaunchTask::create(MinecraftInstance* inst) { - shared_qobject_ptr proc(new LaunchTask(inst)); - proc->init(); - return proc; + auto task = std::unique_ptr(new LaunchTask(inst)); + task->init(); + return task; } -LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstance* instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { @@ -180,7 +180,7 @@ bool LaunchTask::abort() return true; case LaunchTask::NotStarted: { state = LaunchTask::Aborted; - emitFailed("Aborted"); + emitAborted(); return true; } case LaunchTask::Running: diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index db7e453e4..c52273a9e 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -39,7 +39,6 @@ #include #include #include -#include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" @@ -48,21 +47,21 @@ class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(MinecraftInstancePtr instance); + explicit LaunchTask(MinecraftInstance* instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static shared_qobject_ptr create(MinecraftInstancePtr inst); + static std::unique_ptr create(MinecraftInstance* inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - MinecraftInstancePtr instance() { return m_instance; } + MinecraftInstance* instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -119,7 +118,7 @@ class LaunchTask : public Task { bool parseXmlLogs(QString const& line, MessageLevel level); protected: /* data */ - MinecraftInstancePtr m_instance; + MinecraftInstance* m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; diff --git a/launcher/launch/TaskStepWrapper.cpp b/launcher/launch/TaskStepWrapper.cpp index db9e8fad2..acf790a8f 100644 --- a/launcher/launch/TaskStepWrapper.cpp +++ b/launcher/launch/TaskStepWrapper.cpp @@ -23,10 +23,7 @@ void TaskStepWrapper::executeTask() return; } connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); - connect(m_task.get(), &Task::progress, this, &TaskStepWrapper::setProgress); - connect(m_task.get(), &Task::stepProgress, this, &TaskStepWrapper::propagateStepProgress); - connect(m_task.get(), &Task::status, this, &TaskStepWrapper::setStatus); - connect(m_task.get(), &Task::details, this, &TaskStepWrapper::setDetails); + propagateFromOther(m_task.get()); emit progressReportingRequest(); } @@ -59,9 +56,7 @@ bool TaskStepWrapper::canAbort() const bool TaskStepWrapper::abort() { if (m_task && m_task->canAbort()) { - auto status = m_task->abort(); - emitFailed("Aborted."); - return status; + return m_task->abort(); } return Task::abort(); } diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 0f8d27e94..1afd5cd1d 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -36,7 +36,6 @@ #include "CheckJava.h" #include #include -#include #include #include #include @@ -68,7 +67,7 @@ void CheckJava::executeTask() emitFailed(QString("Java path is not valid.")); return; } else { - emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); + emit logLine("Java path is:\n " + m_javaPath, MessageLevel::Launcher); } if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -147,6 +146,6 @@ void CheckJava::checkJavaFinished(const JavaChecker::Result& result) void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor) { emit logLine( - QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n").arg(version, architecture, realArchitecture, vendor), + QString("Java is version %1, using %2 (%3) architecture, from %4").arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp index fdd9fc545..cb2f5d7de 100644 --- a/launcher/launch/steps/LookupServerAddress.cpp +++ b/launcher/launch/steps/LookupServerAddress.cpp @@ -38,7 +38,7 @@ void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) bool LookupServerAddress::abort() { m_dnsLookup->abort(); - emitFailed("Aborted"); + emitAborted(); return true; } diff --git a/launcher/launch/steps/PrintServers.cpp b/launcher/launch/steps/PrintServers.cpp index ba96d37b9..ac0e4bf83 100644 --- a/launcher/launch/steps/PrintServers.cpp +++ b/launcher/launch/steps/PrintServers.cpp @@ -34,7 +34,7 @@ void PrintServers::executeTask() void PrintServers::resolveServer(const QHostInfo& host_info) { QString server = host_info.hostName(); - QString addresses = server + " resolves to:\n ["; + QString addresses = server + " resolves to:\n "; if (!host_info.addresses().isEmpty()) { for (QHostAddress address : host_info.addresses()) { @@ -46,7 +46,7 @@ void PrintServers::resolveServer(const QHostInfo& host_info) } else { addresses += "N/A"; } - addresses += "]\n\n"; + addresses += "\n"; m_server_to_address.insert(server, addresses); diff --git a/launcher/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp index 53aff807e..f96d11343 100644 --- a/launcher/launch/steps/TextPrint.cpp +++ b/launcher/launch/steps/TextPrint.cpp @@ -24,6 +24,6 @@ bool TextPrint::canAbort() const bool TextPrint::abort() { - emitFailed("Aborted."); + emitAborted(); return true; } diff --git a/launcher/main.cpp b/launcher/main.cpp index 2bce655d2..9378387bb 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -33,13 +33,23 @@ * limitations under the License. */ +#include + #include "Application.h" +#if defined Q_OS_WIN32 +#include "console/WindowsConsole.h" +#endif + int main(int argc, char* argv[]) { +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + console::WindowsConsoleGuard _consoleGuard; +#endif + // initialize Qt Application app(argc, argv); - switch (app.status()) { case Application::StartingUp: case Application::Initialized: { diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index b0e754ada..1869e14d3 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -26,6 +26,7 @@ #include "net/NetJob.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "BuildConfig.h" #include "tasks/Task.h" @@ -81,12 +82,12 @@ QUrl BaseEntity::url() const return QUrl(metaOverride).resolved(localFilename()); } -Task::Ptr BaseEntity::loadTask(Net::Mode mode) +Task::Ptr BaseEntity::loadTask(Net::Mode mode, bool forceReload) { if (m_task && m_task->isRunning()) { return m_task; } - m_task.reset(new BaseEntityLoadTask(this, mode)); + m_task.reset(new BaseEntityLoadTask(this, mode, forceReload)); return m_task; } @@ -106,7 +107,9 @@ BaseEntity::LoadStatus BaseEntity::status() const return m_load_status; } -BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} +BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload) + : m_entity(parent), m_mode(mode), m_force_reload(forceReload) +{} void BaseEntityLoadTask::executeTask() { @@ -124,9 +127,11 @@ void BaseEntityLoadTask::executeTask() } // on online the hash needs to match - hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; + const auto& expected = m_entity->m_sha256; + const auto& actual = m_entity->m_file_sha256; + hashMatches = expected == actual; if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { - throw Exception("mismatched checksum"); + throw Exception(QString("Checksum mismatch, expected sha256: %1, got: %2").arg(expected, actual)); } // load local file @@ -148,13 +153,18 @@ void BaseEntityLoadTask::executeTask() auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; - if (wasLoadedOffline || wasLoadedRemote) { + if (wasLoadedOffline || (wasLoadedRemote && !m_force_reload)) { emitSucceeded(); return; } m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); auto url = m_entity->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); + if (m_force_reload) { + // clear validators so manual refreshes fetch a fresh body + entry->setETag({}); + entry->setRemoteChangedTimestamp({}); + } entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h index 17aa0cb87..32d8bdbb8 100644 --- a/launcher/meta/BaseEntity.h +++ b/launcher/meta/BaseEntity.h @@ -43,7 +43,7 @@ class BaseEntity { void setSha256(QString sha256); virtual void parse(const QJsonObject& obj) = 0; - [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); + [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online, bool forceReload = false); protected: QString m_sha256; // the expected sha256 @@ -58,7 +58,7 @@ class BaseEntityLoadTask : public Task { Q_OBJECT public: - explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); + explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode, bool forceReload); ~BaseEntityLoadTask() override = default; virtual void executeTask() override; @@ -68,6 +68,7 @@ class BaseEntityLoadTask : public Task { private: BaseEntity* m_entity; Net::Mode m_mode; + bool m_force_reload = false; NetJob::Ptr m_task; }; } // namespace Meta diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index d0c7075cd..bd58215c4 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -15,6 +15,7 @@ #include "Index.h" +#include "Application.h" #include "JsonFormat.h" #include "QObjectPtr.h" #include "VersionList.h" @@ -135,7 +136,7 @@ void Index::connectVersionList(const int row, const VersionList::Ptr& list) Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) { - if (mode == Net::Mode::Offline) { + if (mode == Net::Mode::Offline || !APPLICATION->settings()->get("MetaRefreshOnLaunch").toBool()) { return get(uid, version)->loadTask(mode); } diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index dfca52d87..fa3a271a6 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -32,11 +32,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( setObjectName("Version list: " + uid); } -Task::Ptr VersionList::getLoadTask() +Task::Ptr VersionList::getLoadTask(bool forceReload) { auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); - loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); - loadTask->addTask(this->loadTask(Net::Mode::Online)); + loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online, forceReload)); + loadTask->addTask(this->loadTask(Net::Mode::Online, forceReload)); return loadTask; } diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 18681b8ed..709c361de 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -37,7 +37,7 @@ class VersionList : public BaseVersionList, public BaseEntity { enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; bool isLoaded() override; - Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask(bool forceReload = false) override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; diff --git a/launcher/minecraft/Agent.h b/launcher/minecraft/Agent.h index bc385a74e..2432679da 100644 --- a/launcher/minecraft/Agent.h +++ b/launcher/minecraft/Agent.h @@ -4,26 +4,10 @@ #include "Library.h" -class Agent; - -using AgentPtr = std::shared_ptr; - -class Agent { - public: - Agent(LibraryPtr library, const QString& argument) - { - m_library = library; - m_argument = argument; - } - - public: /* methods */ - LibraryPtr library() { return m_library; } - QString argument() { return m_argument; } - - protected: /* data */ +struct Agent { /// The library pointing to the jar this Java agent is contained within - LibraryPtr m_library; + LibraryPtr library; /// The argument to the Java agent, passed after an = if present - QString m_argument; + QString argument; }; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 410d1e689..37d02b5c1 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -52,6 +52,7 @@ #include "Application.h" #include "net/NetRequest.h" +#include "update/AssetUpdateTask.h" namespace { QSet collectPathsFromDir(QString dirPath) @@ -103,7 +104,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to read assets index file" << path; + qCritical() << "Failed to read assets index file" << path << "error:" << file.errorString(); return false; } index.id = assetsId; @@ -298,7 +299,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + auto resourceURL = AssetUpdateTask::resourceUrl(); return resourceURL + getRelPath(); } diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index bdfdcfbcc..73203f74b 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -38,6 +38,9 @@ * If the component list changes, start over. */ +/* + * TODO: This task launches multiple other tasks. As such it should be converted to a ConcurrentTask + */ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); @@ -48,9 +51,38 @@ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfi ComponentUpdateTask::~ComponentUpdateTask() {} +bool ComponentUpdateTask::canAbort() const +{ + for (const auto& status : d->remoteLoadStatusList) { + if (status.task && !status.task->canAbort()) { + return false; + } + } + + return true; +} + +bool ComponentUpdateTask::abort() +{ + bool aborted = true; + for (const auto& status : d->remoteLoadStatusList) { + if (status.task && !status.task->abort()) { + aborted = false; + } + } + + return aborted; +} + +Net::Mode ComponentUpdateTask::netMode() +{ + return d->netmode; +} + void ComponentUpdateTask::executeTask() { qCDebug(instanceProfileResolveC) << "Loading components"; + setStatus(tr("Loading components")); loadComponents(); } @@ -196,12 +228,13 @@ void ComponentUpdateTask::loadComponents() componentIndex++; } d->remoteTasksInProgress = taskIndex; + m_progressTotal = static_cast(taskIndex); switch (result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. performUpdateActions(); resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); - break; + return; } case LoadResult::RequiresRemote: { // we wait for signals. @@ -209,9 +242,11 @@ void ComponentUpdateTask::loadComponents() } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); - break; + return; } } + + setDetails(tr("Downloading metadata for %1 components").arg(taskIndex)); } namespace { @@ -754,6 +789,7 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) void ComponentUpdateTask::checkIfAllFinished() { + setProgress(m_progress + 1, m_progressTotal); if (d->remoteTasksInProgress) { // not yet... return; diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h index 64c55877b..2ef9737ba 100644 --- a/launcher/minecraft/ComponentUpdateTask.h +++ b/launcher/minecraft/ComponentUpdateTask.h @@ -17,8 +17,12 @@ class ComponentUpdateTask : public Task { explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); + bool canAbort() const override; + bool abort() override; + Net::Mode netMode(); + protected: - void executeTask(); + void executeTask() override; private: void loadComponents(); diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index a2588064f..65297abed 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -38,12 +38,10 @@ #include #include #include -#include "DefaultVariable.h" struct GradleSpecifier { GradleSpecifier() { m_valid = false; } - GradleSpecifier(QString value) { operator=(value); } - GradleSpecifier& operator=(const QString& value) + GradleSpecifier(const QString& value) { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar @@ -62,7 +60,7 @@ struct GradleSpecifier { m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; - return *this; + return; } auto elements = match.captured(); m_groupId = match.captured(1); @@ -72,7 +70,6 @@ struct GradleSpecifier { if (match.lastCapturedIndex() >= 5) { m_extension = match.captured(5); } - return *this; } QString serialize() const { @@ -83,8 +80,8 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { retval += ":" + m_classifier; } - if (m_extension.isExplicit()) { - retval += "@" + m_extension; + if (m_extension.has_value()) { + retval += "@" + m_extension.value(); } return retval; } @@ -97,7 +94,7 @@ struct GradleSpecifier { if (!m_classifier.isEmpty()) { filename += "-" + m_classifier; } - filename += "." + m_extension; + filename += "." + m_extension.value_or("jar"); return filename; } QString toPath(const QString& filenameOverride = QString()) const @@ -122,26 +119,13 @@ struct GradleSpecifier { inline QString artifactId() const { return m_artifactId; } inline void setClassifier(const QString& classifier) { m_classifier = classifier; } inline QString classifier() const { return m_classifier; } - inline QString extension() const { return m_extension; } + inline std::optional extension() const { return m_extension; } inline QString artifactPrefix() const { return m_groupId + ":" + m_artifactId; } bool matchName(const GradleSpecifier& other) const { return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } - bool operator==(const GradleSpecifier& other) const - { - if (m_groupId != other.m_groupId) - return false; - if (m_artifactId != other.m_artifactId) - return false; - if (m_version != other.m_version) - return false; - if (m_classifier != other.m_classifier) - return false; - if (m_extension != other.m_extension) - return false; - return true; - } + bool operator ==(const GradleSpecifier &other) const = default; private: QString m_invalidValue; @@ -149,6 +133,6 @@ struct GradleSpecifier { QString m_artifactId; QString m_version; QString m_classifier; - DefaultVariable m_extension = DefaultVariable("jar"); + std::optional m_extension; bool m_valid = false; }; diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index c11a0f915..fb74d4a9a 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -213,9 +213,9 @@ void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& r m_mavenFiles.append(Library::limitedCopy(mavenFile)); } -void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext) +void LaunchProfile::applyAgent(const Agent& agent, const RuntimeContext& runtimeContext) { - auto lib = agent->library(); + auto lib = agent.library; if (!lib->isActive(runtimeContext)) { return; } @@ -330,7 +330,7 @@ const QList& LaunchProfile::getMavenFiles() const return m_mavenFiles; } -const QList& LaunchProfile::getAgents() const +const QList& LaunchProfile::getAgents() const { return m_agents; } @@ -349,7 +349,8 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath) const + const QString& tempPath, + bool addJarMods) const { QStringList native32, native64; jars.clear(); @@ -360,7 +361,7 @@ void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, // NOTE: order is important here, add main jar last to the lists if (m_mainJar) { // FIXME: HACK!! jar modding is weird and unsystematic! - if (m_jarMods.size()) { + if (m_jarMods.size() && addJarMods) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index f1be6fee0..6dc3d9aeb 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -57,7 +57,7 @@ class LaunchProfile : public ProblemProvider { void applyMods(const QList& jarMods); void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); - void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); + void applyAgent(const Agent& agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); @@ -79,7 +79,7 @@ class LaunchProfile : public ProblemProvider { const QList& getLibraries() const; const QList& getNativeLibraries() const; const QList& getMavenFiles() const; - const QList& getAgents() const; + const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; @@ -87,7 +87,8 @@ class LaunchProfile : public ProblemProvider { QStringList& jars, QStringList& nativeJars, const QString& overridePath, - const QString& tempPath) const; + const QString& tempPath, + bool addJarMods = true) const; bool hasTrait(const QString& trait) const; ProblemSeverity getProblemSeverity() const override; const QList getProblems() const override; @@ -132,7 +133,7 @@ class LaunchProfile : public ProblemProvider { QList m_mavenFiles; /// the list of java agents to add to JVM arguments - QList m_agents; + QList m_agents; /// the main jar LibraryPtr m_mainJar; diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 026f9c281..2b43d4389 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -149,7 +149,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC if (sha1.size()) { auto dl = Net::ApiDownload::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); - qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url << "expected sha1:" << sha1; out.append(dl); } else { out.append(Net::ApiDownload::makeCached(url, entry, options)); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index f8924579a..8e98a2efe 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -59,6 +59,7 @@ #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" @@ -69,8 +70,8 @@ #include "minecraft/launch/VerifyJavaInstall.h" #include "minecraft/update/AssetUpdateTask.h" -#include "minecraft/update/FMLLibrariesTask.h" #include "minecraft/update/FoldersTask.h" +#include "minecraft/update/LegacyFMLLibrariesTask.h" #include "minecraft/update/LibrariesTask.h" #include "java/JavaUtils.h" @@ -97,7 +98,7 @@ #include #ifdef Q_OS_LINUX -#include "MangoHud.h" +#include "LibraryUtils.h" #endif #ifdef WITH_QTDBUS @@ -130,7 +131,8 @@ for (const auto& gpu : gpus) { QString name = qvariant_cast(gpu[QStringLiteral("Name")]); bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); - if (!defaultGpu) { + bool discrete = qvariant_cast(gpu.value(QStringLiteral("Discrete"), !defaultGpu)); + if (discrete) { QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); for (int i = 0; i + 1 < envList.size(); i += 2) { env.insert(envList[i], envList[i + 1]); @@ -162,12 +164,14 @@ class OrSetting : public Setting { std::shared_ptr m_b; }; -MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) - : BaseInstance(globalSettings, settings, rootDir) +MinecraftInstance::MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) + : BaseInstance(globalSettings, std::move(settings), rootDir) { m_components.reset(new PackProfile(this)); } +MinecraftInstance::~MinecraftInstance() {} + void MinecraftInstance::saveNow() { m_components->saveNow(); @@ -206,6 +210,7 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("LowMemWarning"), memorySetting); // Native library workarounds auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); @@ -271,7 +276,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { - m_runtimeContext.updateFromInstanceSettings(m_settings); + m_runtimeContext.updateFromInstanceSettings(m_settings.get()); m_components->invalidateLaunchProfile(); } @@ -280,9 +285,9 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -std::shared_ptr MinecraftInstance::getPackProfile() const +PackProfile* MinecraftInstance::getPackProfile() const { - return m_components; + return m_components.get(); } QSet MinecraftInstance::traits() const @@ -310,9 +315,9 @@ void MinecraftInstance::populateLaunchMenu(QMenu* menu) normalLaunchDemo->setEnabled(supportsDemo()); - connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this()); }); - connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, false); }); - connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, true); }); + connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(this); }); + connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Offline); }); + connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(this, LaunchMode::Demo); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); @@ -527,10 +532,10 @@ QStringList MinecraftInstance::extraArguments() } } auto agents = m_components->getProfile()->getAgents(); - for (auto agent : agents) { + for (const auto& agent : agents) { QStringList jar, temp1, temp2, temp3; - agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); - list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); + agent.library->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); + list.append("-javaagent:" + jar[0] + (agent.argument.isEmpty() ? "" : "=" + agent.argument)); } { @@ -566,6 +571,8 @@ QStringList MinecraftInstance::javaArguments() { QStringList args; + args << "-Duser.language=en"; + // custom args go first. we want to override them if we have our own here. args.append(extraArguments()); @@ -619,8 +626,6 @@ QStringList MinecraftInstance::javaArguments() } } - args << "-Duser.language=en"; - if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; @@ -649,7 +654,7 @@ QMap MinecraftInstance::getVariables() out.insert("INST_ID", id()); out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); - out.insert("INST_JAVA", settings()->get("JavaPath").toString()); + out.insert("INST_JAVA", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); #ifdef Q_OS_MACOS @@ -683,12 +688,7 @@ QProcessEnvironment MinecraftInstance::createEnvironment() env.insert(iter.key(), iter.value().toString()); }; - bool overrideEnv = settings()->get("OverrideEnv").toBool(); - - if (!overrideEnv) - insertEnv(APPLICATION->settings()->get("Env").toString()); - else - insertEnv(settings()->get("Env").toString()); + insertEnv(settings()->get("Env").toString()); return env; } @@ -703,7 +703,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); - auto mangoHudLibString = MangoHud::getLibraryString(); + auto mangoHudLibString = LibraryUtils::findMangoHud(); if (!mangoHudLibString.isEmpty()) { QFileInfo mangoHudLib(mangoHudLibString); QString libPath = mangoHudLib.absolutePath(); @@ -739,6 +739,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); env.insert("GALLIUM_DRIVER", "zink"); + env.insert("LIBGL_KOPPER_DRI2", "1"); } #endif return env; @@ -747,21 +748,21 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); - QString args_pattern = profile->getMinecraftArguments(); + auto args = profile->getMinecraftArguments().split(' ', Qt::SkipEmptyParts); for (auto tweaker : profile->getTweakers()) { - args_pattern += " --tweakClass " + tweaker; + args << "--tweakClass" << tweaker; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { if (profile->hasTrait("feature:is_quick_play_multiplayer")) { - args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + args << "--quickPlayMultiplayer" << targetToJoin->address + ':' + QString::number(targetToJoin->port); } else { - args_pattern += " --server " + targetToJoin->address; - args_pattern += " --port " + QString::number(targetToJoin->port); + args << "--server" << targetToJoin->address; + args << "--port" << QString::number(targetToJoin->port); } } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { - args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; + args << "--quickPlaySingleplayer" << targetToJoin->world; } } @@ -777,16 +778,15 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine tokenMapping["user_properties"] = session->serializeUserProperties(); tokenMapping["user_type"] = session->user_type; - if (session->demo) { - args_pattern += " --demo"; + if (session->launchMode == LaunchMode::Demo) { + args << "--demo"; } } - QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); - for (int i = 0; i < parts.length(); i++) { - parts[i] = replaceTokensIn(parts[i], tokenMapping); + for (int i = 0; i < args.length(); i++) { + args[i] = replaceTokensIn(args[i], tokenMapping); } - return parts; + return args; } QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) @@ -888,50 +888,24 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { + constexpr auto indent = " "; + constexpr auto emptyLine = ""; + QStringList out; - out << "Main Class:" << " " + getMainClass() << ""; - out << "Native path:" << " " + getNativePath() << ""; + + out << "Components:"; + for (int i = 0; i < m_components->rowCount(); ++i) { + const auto& component = m_components->getComponent(i); + out << indent + + QString("%1) %2 (%3) %4").arg(QString::number(i + 1), component->getName(), component->getID(), component->getVersion()); + } + out << emptyLine; + + out << "Launcher: " + getLauncher(); + out << "Main class: " + getMainClass() << emptyLine; auto profile = m_components->getProfile(); - // traits - auto alltraits = traits(); - if (alltraits.size()) { - out << "Traits:"; - for (auto trait : alltraits) { - out << "traits " + trait; - } - out << ""; - } - - // native libraries - auto settings = this->settings(); - bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); - bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); - if (nativeOpenAL || nativeGLFW) { - if (nativeOpenAL) - out << "Using system OpenAL."; - if (nativeGLFW) - out << "Using system GLFW."; - out << ""; - } - - // libraries and class path. - { - out << "Libraries:"; - QStringList jars, nativeJars; - profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); - for (auto file : jars) { - out << " " + file; - } - out << ""; - out << "Native libraries:"; - for (auto file : nativeJars) { - out << " " + file; - } - out << ""; - } - // mods and core mods auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { @@ -954,12 +928,12 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } - out << ""; + out << emptyLine; } }; - printModList("Mods", *(loaderModList().get())); - printModList("Core Mods", *(coreModList().get())); + printModList("Mods", *loaderModList()); + printModList("Core Mods", *coreModList()); // jar mods auto& jarMods = profile->getJarMods(); @@ -969,19 +943,59 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto displayname = jarmod->displayName(runtimeContext()); auto realname = jarmod->filename(runtimeContext()); if (displayname != realname) { - out << " " + displayname + " (" + realname + ")"; + out << indent + displayname + " (" + realname + ")"; } else { - out << " " + realname; + out << indent + realname; } } - out << ""; + out << emptyLine; } + // traits + auto alltraits = traits(); + if (alltraits.size()) { + out << "Traits:"; + for (auto trait : alltraits) { + out << indent + trait; + } + out << emptyLine; + } + + // native libraries + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << emptyLine; + } + + // libraries and class path. + { + out << "Libraries:"; + QStringList jars, nativeJars; + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + for (auto file : jars) { + out << indent + file; + } + out << emptyLine; + out << "Native libraries:"; + for (auto file : nativeJars) { + out << indent + file; + } + out << emptyLine; + } + + out << "Natives path:" << indent + getNativePath() << emptyLine; + // minecraft arguments auto params = processMinecraftArgs(nullptr, targetToJoin); - out << "Params:"; - out << " " + params.join(' '); - out << ""; + out << "Minecraft arguments:"; + out << indent + params.join(' '); + out << emptyLine; // window size QString windowParams; @@ -992,9 +1006,18 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } - out << ""; - out << "Launcher: " + getLauncher(); - out << ""; + out << emptyLine; + + // environment variables + const QString env = settings->get("Env").toString(); + if (auto envMap = Json::toMap(env); !envMap.isEmpty()) { + out << "Custom environment variables:"; + for (auto [key, value] : envMap.asKeyValueRange()) { + out << indent + key + "=" + value.toString(); + } + out << emptyLine; + } + return out; } @@ -1094,24 +1117,23 @@ QList MinecraftInstance::createUpdateTask() // libraries download makeShared(this), // FML libraries download and copy into the instance - makeShared(this), + makeShared(this), // assets update makeShared(this), }; } -shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); - // FIXME: get rid of shared_from_this ... - auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_this())); + auto process = LaunchTask::create(this); auto pptr = process.get(); APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { - process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); + process->appendStep(makeShared(pptr, "Minecraft folder is:\n " + gameRoot() + "\n", MessageLevel::Launcher)); } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) @@ -1141,7 +1163,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // load meta { - auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; + auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; process->appendStep(makeShared(pptr, makeShared(this, mode))); } @@ -1149,6 +1171,8 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->appendStep(makeShared(pptr)); process->appendStep(makeShared(pptr)); + // verify that minimum Java requirements are met + process->appendStep(makeShared(pptr)); } // run pre-launch command if that's needed @@ -1158,11 +1182,9 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } - // if we aren't in offline mode,. - if (session->status != AuthSession::PlayableOffline) { - if (!session->demo) { - process->appendStep(makeShared(pptr, session)); - } + // if we aren't in offline mode + if (session->launchMode != LaunchMode::Offline) { + process->appendStep(makeShared(pptr, session)); for (auto t : createUpdateTask()) { process->appendStep(makeShared(pptr, t)); } @@ -1180,6 +1202,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } + // make sure we have enough RAM, warn the user if we don't + { + process->appendStep(makeShared(pptr, this)); + } + // print some instance info here... { process->appendStep(makeShared(pptr, session, targetToJoin)); @@ -1195,11 +1222,6 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr)); } - // verify that minimum Java requirements are met - { - process->appendStep(makeShared(pptr)); - } - { // actually launch the game auto step = makeShared(pptr); @@ -1221,9 +1243,9 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if (m_settings->get("QuitAfterGameStop").toBool()) { process->appendStep(makeShared(pptr)); } - m_launchProcess = process; - emit launchTaskChanged(m_launchProcess); - return m_launchProcess; + m_launchProcess = std::move(process); + emit launchTaskChanged(m_launchProcess.get()); + return m_launchProcess.get(); } JavaVersion MinecraftInstance::getJavaVersion() @@ -1231,80 +1253,80 @@ JavaVersion MinecraftInstance::getJavaVersion() return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr MinecraftInstance::loaderModList() +ModFolderModel* MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); } - return m_loader_mod_list; + return m_loader_mod_list.get(); } -std::shared_ptr MinecraftInstance::coreModList() +ModFolderModel* MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); } - return m_core_mod_list; + return m_core_mod_list.get(); } -std::shared_ptr MinecraftInstance::nilModList() +ModFolderModel* MinecraftInstance::nilModList() { if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); } - return m_nil_mod_list; + return m_nil_mod_list.get(); } -std::shared_ptr MinecraftInstance::resourcePackList() +ResourcePackFolderModel* MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); } - return m_resource_pack_list; + return m_resource_pack_list.get(); } -std::shared_ptr MinecraftInstance::texturePackList() +TexturePackFolderModel* MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); } - return m_texture_pack_list; + return m_texture_pack_list.get(); } -std::shared_ptr MinecraftInstance::shaderPackList() +ShaderPackFolderModel* MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); } - return m_shader_pack_list; + return m_shader_pack_list.get(); } -std::shared_ptr MinecraftInstance::dataPackList() +DataPackFolderModel* MinecraftInstance::dataPackList() { if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); } - return m_data_pack_list; + return m_data_pack_list.get(); } -QList> MinecraftInstance::resourceLists() +QList MinecraftInstance::resourceLists() { return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } -std::shared_ptr MinecraftInstance::worldList() +WorldList* MinecraftInstance::worldList() { if (!m_world_list) { m_world_list.reset(new WorldList(worldDir(), this)); } - return m_world_list; + return m_world_list.get(); } QList MinecraftInstance::getJarMods() const diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index ecddef69c..909962d5e 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -56,8 +56,8 @@ class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: - MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); - virtual ~MinecraftInstance() = default; + MinecraftInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir); + virtual ~MinecraftInstance(); virtual void saveNow() override; void loadSpecificSettings() override; @@ -109,22 +109,22 @@ class MinecraftInstance : public BaseInstance { void updateRuntimeContext() override; ////// Profile management ////// - std::shared_ptr getPackProfile() const; + PackProfile* getPackProfile() const; ////// Mod Lists ////// - std::shared_ptr loaderModList(); - std::shared_ptr coreModList(); - std::shared_ptr nilModList(); - std::shared_ptr resourcePackList(); - std::shared_ptr texturePackList(); - std::shared_ptr shaderPackList(); - std::shared_ptr dataPackList(); - QList> resourceLists(); - std::shared_ptr worldList(); + ModFolderModel* loaderModList(); + ModFolderModel* coreModList(); + ModFolderModel* nilModList(); + ResourcePackFolderModel* resourcePackList(); + TexturePackFolderModel* texturePackList(); + ShaderPackFolderModel* shaderPackList(); + DataPackFolderModel* dataPackList(); + QList resourceLists(); + WorldList* worldList(); ////// Launch stuff ////// QList createUpdateTask() override; - shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; + LaunchTask* createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; @@ -162,15 +162,13 @@ class MinecraftInstance : public BaseInstance { QMap makeProfileVarMapping(std::shared_ptr profile) const; protected: // data - std::shared_ptr m_components; - mutable std::shared_ptr m_loader_mod_list; - mutable std::shared_ptr m_core_mod_list; - mutable std::shared_ptr m_nil_mod_list; - mutable std::shared_ptr m_resource_pack_list; - mutable std::shared_ptr m_shader_pack_list; - mutable std::shared_ptr m_texture_pack_list; - mutable std::shared_ptr m_data_pack_list; - mutable std::shared_ptr m_world_list; + std::unique_ptr m_components; + std::unique_ptr m_loader_mod_list; + std::unique_ptr m_core_mod_list; + std::unique_ptr m_nil_mod_list; + std::unique_ptr m_resource_pack_list; + std::unique_ptr m_shader_pack_list; + std::unique_ptr m_texture_pack_list; + std::unique_ptr m_data_pack_list; + std::unique_ptr m_world_list; }; - -using MinecraftInstancePtr = std::shared_ptr; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index 149e7cf19..c26fb8b60 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -20,11 +20,8 @@ void MinecraftLoadAndCheck::executeTask() } connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); - connect(m_task.get(), &Task::aborted, this, [this] { emitFailed(tr("Aborted")); }); - connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); - connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); - connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); - connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); + connect(m_task.get(), &Task::aborted, this, &MinecraftLoadAndCheck::emitAborted); + propagateFromOther(m_task.get()); } bool MinecraftLoadAndCheck::canAbort() const @@ -38,9 +35,7 @@ bool MinecraftLoadAndCheck::canAbort() const bool MinecraftLoadAndCheck::abort() { if (m_task && m_task->canAbort()) { - auto status = m_task->abort(); - emitFailed("Aborted."); - return status; + return m_task->abort(); } return Task::abort(); } diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 48ea3b894..95f4c5ef6 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -209,8 +209,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc QString arg = ""; readString(agentObj, "argument", arg); - AgentPtr agent(new Agent(lib, arg)); - out->agents.append(agent); + out->agents.append(Agent{ lib, arg }); } } @@ -305,10 +304,10 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch writeStringList(root, "+jvmArgs", patch->addnJvmArguments); if (!patch->agents.isEmpty()) { QJsonArray array; - for (auto value : patch->agents) { - QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); - if (!value->argument().isEmpty()) - agentOut.insert("argument", value->argument()); + for (const auto& value : patch->agents) { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value.library.get()); + if (!value.argument.isEmpty()) + agentOut.insert("argument", value.argument); array.append(agentOut); } @@ -370,8 +369,7 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemConta } // just make up something unique on the spot for the library name. - auto uuid = QUuid::createUuid(); - QString id = uuid.toString().remove('{').remove('}'); + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); // filename override is the old name diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 7ffebddb5..f0cff7f0e 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -326,7 +326,15 @@ PackProfile::Result PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if (d->m_updateTask) { - return Result::Success(); + if (d->m_updateTask->netMode() == netmode) { + return Result::Success(); + } + + // https://github.com/PrismLauncher/PrismLauncher/issues/5209 + // FIXME: HACK HACK HACK + disconnect(d->m_updateTask.get(), &ComponentUpdateTask::aborted, nullptr, nullptr); + d->m_updateTask->abort(); + d->m_updateTask.reset(); } // flush any scheduled saves to not lose state @@ -917,7 +925,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths) agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); - versionFile->agents.append(std::make_shared(agent, QString())); + versionFile->agents.append(Agent{agent, QString()}); versionFile->name = targetName; versionFile->uid = targetId; diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h index 4fb3621f0..feb825904 100644 --- a/launcher/minecraft/PackProfile_p.h +++ b/launcher/minecraft/PackProfile_p.h @@ -22,7 +22,7 @@ struct PackProfileData { ComponentIndex componentIndex; bool dirty = false; QTimer m_saveTimer; - Task::Ptr m_updateTask; + shared_qobject_ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index b0b9122e3..ae6326953 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -144,13 +144,13 @@ bool saveJsonFile(const QJsonDocument& doc, const QString& filename) auto data = doc.toJson(); QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { + qWarning() << "Couldn't open" << filename << "for writing:" << jsonFile.errorString(); jsonFile.cancelWriting(); - qWarning() << "Couldn't open" << filename << "for writing"; return false; } jsonFile.write(data); if (!jsonFile.commit()) { - qWarning() << "Couldn't save" << filename; + qWarning() << "Couldn't save" << filename << "error:" << jsonFile.errorString(); return false; } return true; diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index ec4ebb31a..b719e3142 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -71,7 +71,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application: %1").arg(iconFile.errorString())); return false; } @@ -101,7 +101,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); @@ -127,7 +127,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut: %1").arg(iconFile.errorString())); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index b995c36bd..5cf31f9b2 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -37,6 +37,7 @@ #pragma once #include "Application.h" +#include "BaseInstance.h" #include #include diff --git a/launcher/minecraft/VanillaInstanceCreationTask.cpp b/launcher/minecraft/VanillaInstanceCreationTask.cpp index ccbd8c677..e646e2c52 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.cpp +++ b/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -7,32 +7,27 @@ #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" -VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) - : InstanceCreationTask() - , m_version(std::move(version)) - , m_using_loader(true) - , m_loader(std::move(loader)) - , m_loader_version(std::move(loader_version)) +VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion) + : m_version(std::move(version)), m_using_loader(true), m_loader(std::move(loader)), m_loader_version(std::move(loaderVersion)) {} -bool VanillaCreationTask::createInstance() +std::unique_ptr VanillaCreationTask::createInstance() { setStatus(tr("Creating instance from version %1").arg(m_version->name())); - auto instance_settings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); - instance_settings->suspendSave(); - { - MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath); - auto components = inst.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", m_version->descriptor(), true); - if (m_using_loader) - components->setComponentVersion(m_loader, m_loader_version->descriptor()); + auto inst = std::make_unique( + m_globalSettings, std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), m_stagingPath); + SettingsObject::Lock lock(inst->settings()); - inst.setName(name()); - inst.setIconKey(m_instIcon); + auto* components = inst->getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if (m_using_loader) { + components->setComponentVersion(m_loader, m_loader_version->descriptor()); } - instance_settings->resumeSave(); - return true; + inst->setName(name()); + inst->setIconKey(m_instIcon); + components->saveNow(); + return inst; } diff --git a/launcher/minecraft/VanillaInstanceCreationTask.h b/launcher/minecraft/VanillaInstanceCreationTask.h index d1b816824..c1a69ab62 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.h +++ b/launcher/minecraft/VanillaInstanceCreationTask.h @@ -7,10 +7,10 @@ class VanillaCreationTask final : public InstanceCreationTask { Q_OBJECT public: - VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} - VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); + explicit VanillaCreationTask(BaseVersion::Ptr version) : m_version(std::move(version)) {} + VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loaderVersion); - bool createInstance() override; + std::unique_ptr createInstance() override; private: // Version to update to / create of the instance. diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 40f49aaa4..32a7504ac 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -126,7 +126,7 @@ class VersionFile : public ProblemContainer { QList mavenFiles; /// Prism Launcher: list of agents to add to JVM arguments - QList agents; + QList agents; /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index fca45fb99..0deecb042 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -153,18 +153,23 @@ QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) return val; } -QString getLevelDatFromFS(const QFileInfo& file) +QString getDatFromFS(const QFileInfo& root, QString file) { - QDir worldDir(file.filePath()); - if (!file.isDir() || !worldDir.exists("level.dat")) { + QDir worldDir(root.filePath()); + if (!root.isDir() || !worldDir.exists(file)) { return QString(); } - return worldDir.absoluteFilePath("level.dat"); + return worldDir.absoluteFilePath(file); } -QByteArray getLevelDatDataFromFS(const QFileInfo& file) +QString getLevelDatFromFS(const QFileInfo& file) { - auto fullFilePath = getLevelDatFromFS(file); + return getDatFromFS(file, "level.dat"); +} + +QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +{ + auto fullFilePath = getDatFromFS(root, file); if (fullFilePath.isNull()) { return QByteArray(); } @@ -175,6 +180,16 @@ QByteArray getLevelDatDataFromFS(const QFileInfo& file) return f.readAll(); } +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "level.dat"); +} + +QByteArray getWorldGenDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); +} + bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) { auto fullFilePath = getLevelDatFromFS(file); @@ -229,6 +244,8 @@ bool World::resetIcon() return false; } +int64_t loadSeed(QByteArray data); + void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); @@ -238,6 +255,12 @@ void World::readFromFS(const QFileInfo& file) } loadFromLevelDat(bytes); m_levelDatTime = file.lastModified(); + if (m_randomSeed == 0) { + bytes = getWorldGenDataFromFS(file); + if (!bytes.isEmpty()) { + m_randomSeed = loadSeed(bytes); + } + } } void World::readFromZip(const QFileInfo& file) @@ -251,9 +274,6 @@ void World::readFromZip(const QFileInfo& file) QFileInfo fi(filePath); if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { m_containerOffsetPath = filePath.chopped(levelDat.length()); - if (!m_containerOffsetPath.isEmpty()) { - return false; - } m_levelDatTime = file->dateTime(); loadFromLevelDat(file->readAll()); m_isValid = true; @@ -393,6 +413,28 @@ GameType read_gametype(nbt::value& parent, const char* name) } // namespace +int64_t loadSeed(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) { + return 0; + } + + nbt::value* valPtr = nullptr; + try { + valPtr = &levelData->at("data"); + } catch (const std::out_of_range&) { + return 0; + } + nbt::value& val = *valPtr; + + try { + return read_long(val, "seed").value_or(0); + } catch (const std::out_of_range&) { + } + return 0; +} + void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 4aa0f7532..ca8ac1aa8 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -157,7 +157,8 @@ bool WorldList::resetIcon(int row) return false; World& m = m_worlds[row]; if (m.resetIcon()) { - emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); + QModelIndex modelIndex = index(row, NameColumn); + emit dataChanged(modelIndex, modelIndex, { WorldList::IconFileRole }); return true; } return false; @@ -426,7 +427,7 @@ void WorldList::loadWorldsAsync() m_worlds[row].setSize(size); // Notify views - QModelIndex modelIndex = index(row); + QModelIndex modelIndex = index(row, SizeColumn); emit dataChanged(modelIndex, modelIndex, { SizeRole }); } }, diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 29a65e275..bfb350a63 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -303,7 +303,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); - xboxApiToken = tokenFromJSONV3(data, "xrp-main"); mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); } @@ -333,7 +332,6 @@ QJsonObject AccountData::saveState() const output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); - tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } else if (type == AccountType::Offline) { output["type"] = "Offline"; @@ -358,28 +356,10 @@ QString AccountData::profileId() const QString AccountData::profileName() const { if (minecraftProfile.name.size() == 0) { - return QObject::tr("No profile (%1)").arg(accountDisplayString()); - } else { - return minecraftProfile.name; + return QObject::tr("No Minecraft profile"); } -} -QString AccountData::accountDisplayString() const -{ - switch (type) { - case AccountType::Offline: { - return QObject::tr(""); - } - case AccountType::MSA: { - if (xboxApiToken.extra.contains("gtg")) { - return xboxApiToken.extra["gtg"].toString(); - } - return "Xbox profile missing"; - } - default: { - return "Invalid Account"; - } - } + return minecraftProfile.name; } QString AccountData::lastError() const diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 3b76012cb..96ef94f30 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -41,6 +41,7 @@ #include #include +#include #include enum class Validity { None, Assumed, Certain }; @@ -95,9 +96,6 @@ struct AccountData { QJsonObject saveState() const; bool resumeStateFromV3(QJsonObject data); - //! userName for Mojang accounts, gamertag for MSA - QString accountDisplayString() const; - //! Yggdrasil access token, as passed to the game. QString accessToken() const; @@ -111,7 +109,6 @@ struct AccountData { QString msaClientID; Token msaToken; Token userToken; - Token xboxApiToken; Token mojangservicesToken; Token yggdrasilToken; @@ -122,5 +119,6 @@ struct AccountData { // runtime only information (not saved with the account) QString internalId; QString errorString; + QNetworkReply::NetworkError networkError = QNetworkReply::NoError; AccountState accountState = AccountState::Unchecked; }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index d86014a34..418ba98d2 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -168,6 +169,26 @@ void AccountList::removeAccount(QModelIndex index) } } +void AccountList::moveAccount(QModelIndex index, int delta) +{ + const int row = index.row(); + const int newRow = row + delta; + if (index.isValid() && row < m_accounts.size() && newRow >= 0 && newRow < m_accounts.size()) { + // Qt is stupid, https://doc.qt.io/qt-6/qabstractitemmodel.html#beginMoveRows + const int modelDestinationRow = (newRow > row) ? newRow + 1 : newRow; + + if (beginMoveRows(QModelIndex(), row, row, QModelIndex(), modelDestinationRow)) { + m_accounts.move(row, newRow); + endMoveRows(); + + onListChanged(); + } else { + qCritical().noquote() << "AccountList: failed to move account from" << row << "to" << newRow + << QString("(%1 accounts in total)").arg(this->count()); + } + } +} + MinecraftAccountPtr AccountList::defaultAccount() const { return m_defaultAccount; @@ -295,12 +316,28 @@ QVariant AccountList::data(const QModelIndex& index, int role) const MinecraftAccountPtr account = at(index.row()); switch (role) { + case Qt::SizeHintRole: + if (index.column() == ProfileNameColumn) { + return QSize(0, 30); + } + + return QVariant(); + case Qt::DecorationRole: + if (index.column() == ProfileNameColumn) { + auto face = account->getFace(24, 24); + + if (!face.isNull()) { + return face; + } else { + return QIcon::fromTheme("noaccount").pixmap(24, 24); + } + } + + return QVariant(); case Qt::DisplayRole: switch (index.column()) { case ProfileNameColumn: return account->profileName(); - case NameColumn: - return account->accountDisplayString(); case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { @@ -318,9 +355,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const return QVariant(); } - case Qt::ToolTipRole: - return account->accountDisplayString(); - case PointerRole: return QVariant::fromValue(account); @@ -341,8 +375,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o switch (section) { case ProfileNameColumn: return tr("Username"); - case NameColumn: - return tr("Account"); case TypeColumn: return tr("Type"); case StatusColumn: @@ -355,8 +387,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o switch (section) { case ProfileNameColumn: return tr("Minecraft username associated with the account."); - case NameColumn: - return tr("User name of the account."); case TypeColumn: return tr("Type of the account (MSA or Offline)"); case StatusColumn: @@ -420,7 +450,7 @@ bool AccountList::loadList() // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { - qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + qCritical() << QString("Failed to read the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } @@ -537,7 +567,7 @@ bool AccountList::saveList() // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::WriteOnly)) { - qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + qCritical() << QString("Failed to save the account list file %1 (%2).").arg(m_listFilePath).arg(file.errorString()).toUtf8(); return false; } @@ -548,7 +578,7 @@ bool AccountList::saveList() qDebug() << "Saved account list to" << m_listFilePath; return true; } else { - qDebug() << "Failed to save accounts to" << m_listFilePath; + qDebug() << "Failed to save accounts to" << m_listFilePath << "error:" << file.errorString(); return false; } } @@ -618,21 +648,32 @@ void AccountList::tryNext() while (m_refreshQueue.length()) { auto accountId = m_refreshQueue.front(); m_refreshQueue.pop_front(); + bool found = false; for (int i = 0; i < count(); i++) { auto account = at(i); if (account->internalId() == accountId) { + found = true; + if (!account->shouldRefresh()) { + // Account no longer needs refreshing, skip it. + qDebug() << "RefreshSchedule: Skipping account" << account->profileName() << "with internal ID" + << accountId << "(no longer needs refresh)"; + break; + } m_currentTask = account->refresh(); if (m_currentTask) { connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); m_currentTask->start(); - qDebug() << "RefreshSchedule: Processing account" << account->accountDisplayString() << "with internal ID" + qDebug() << "RefreshSchedule: Processing account" << account->profileName() << "with internal ID" << accountId; return; } + break; } } - qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; + if (!found) { + qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; + } } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 2f1276312..916f23341 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel { enum VListColumns { // TODO: Add icon column. ProfileNameColumn = 0, - NameColumn, TypeColumn, StatusColumn, @@ -78,6 +77,7 @@ class AccountList : public QAbstractListModel { void addAccount(MinecraftAccountPtr account); void removeAccount(QModelIndex index); + void moveAccount(QModelIndex index, int delta); int findAccountByProfileId(const QString& profileId) const; MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; QStringList profileNames() const; diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index cea171b33..5b8f98122 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -10,7 +10,6 @@ #include "minecraft/auth/steps/MSAStep.h" #include "minecraft/auth/steps/MinecraftProfileStep.h" #include "minecraft/auth/steps/XboxAuthorizationStep.h" -#include "minecraft/auth/steps/XboxProfileStep.h" #include "minecraft/auth/steps/XboxUserStep.h" #include "tasks/Task.h" @@ -32,11 +31,9 @@ AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) m_steps.append(oauthStep); } m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); m_steps.append( makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index 710509d8e..d881a7691 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -2,7 +2,6 @@ #include #include -#include #include #include diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index a74eabb7a..85d77be9c 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -20,23 +20,17 @@ QString AuthSession::serializeUserProperties() bool AuthSession::MakeOffline(QString offline_playername) { - if (status != PlayableOffline && status != PlayableOnline) { - return false; - } session = "-"; access_token = "0"; player_name = offline_playername; - status = PlayableOffline; return true; } void AuthSession::MakeDemo(QString name, QString u) { - wants_online = false; - demo = true; uuid = u; session = "-"; access_token = "0"; player_name = name; - status = PlayableOnline; // needs online to download the assets + launchMode = LaunchMode::Demo; }; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index cbe604805..07db54213 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -3,6 +3,8 @@ #include #include +#include "LaunchMode.h" + class MinecraftAccount; struct AuthSession { @@ -11,16 +13,6 @@ struct AuthSession { QString serializeUserProperties(); - enum Status { - Undetermined, - RequiresOAuth, - RequiresPassword, - RequiresProfileSetup, - PlayableOffline, - PlayableOnline, - GoneOrMigrated - } status = Undetermined; - // combined session ID QString session; // volatile auth token @@ -29,15 +21,10 @@ struct AuthSession { QString player_name; // profile ID QString uuid; - // 'legacy' or 'mojang', depending on account type + // 'msa' or 'offline', depending on account type QString user_type; - // Did the auth server reply? - bool auth_server_online = false; - // Did the user request online mode? - bool wants_online = true; - - // Is this a demo session? - bool demo = false; + // the actual launch mode for this session + LaunchMode launchMode; }; using AuthSessionPtr = std::shared_ptr; diff --git a/launcher/minecraft/auth/AuthStep.h b/launcher/minecraft/auth/AuthStep.h index aaaec6e7f..f8131509f 100644 --- a/launcher/minecraft/auth/AuthStep.h +++ b/launcher/minecraft/auth/AuthStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include #include "QObjectPtr.h" diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index ca052c378..e346f015b 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,8 +55,7 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - static const QRegularExpression s_removeChars("[{}-]"); - data.internalId = QUuid::createUuid().toString().remove(s_removeChars); + data.internalId = QUuid::createUuid().toString(QUuid::Id128); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -77,15 +76,14 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { - static const QRegularExpression s_removeChars("[{}-]"); auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(s_removeChars); - account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128); + account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -101,7 +99,7 @@ AccountState MinecraftAccount::accountState() const return data.accountState; } -QPixmap MinecraftAccount::getFace() const +QPixmap MinecraftAccount::getFace(int width, int height) const { QPixmap skinTexture; if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { @@ -112,7 +110,7 @@ QPixmap MinecraftAccount::getFace() const QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(64, 64, Qt::KeepAspectRatio); + return skin.scaled(width, height, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) @@ -193,6 +191,14 @@ void MinecraftAccount::authFailed(QString reason) emit activityChanged(false); } +QString MinecraftAccount::displayName() const +{ + if (const QList validStates{ AccountState::Unchecked, AccountState::Working, AccountState::Offline, AccountState::Online }; !validStates.contains(accountState())) { + return QString("⚠ %1").arg(profileName()); + } + return profileName(); +} + bool MinecraftAccount::isActive() const { return !m_currentTask.isNull(); @@ -235,17 +241,6 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { - static const QRegularExpression s_removeChars("[{}-]"); - if (ownsMinecraft() && !hasProfile()) { - session->status = AuthSession::RequiresProfileSetup; - } else { - if (session->wants_online) { - session->status = AuthSession::PlayableOnline; - } else { - session->status = AuthSession::PlayableOffline; - } - } - // volatile auth token session->access_token = data.accessToken(); // profile name @@ -253,7 +248,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); + session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { @@ -291,12 +286,12 @@ QUuid MinecraftAccount::uuidFromUsername(QString username) // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); - auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; - auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; - bAnd(digest, 6, (char)0x0f); // clear version - bOr(digest, 6, (char)0x30); // set to version 3 - bAnd(digest, 8, (char)0x3f); // clear variant - bOr(digest, 8, (char)0x80); // set to IETF variant + auto bOr = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] &= value; }; + bAnd(digest, 6, 0x0f); // clear version + bOr(digest, 6, 0x30); // set to version 3 + bAnd(digest, 8, 0x3f); // clear variant + bOr(digest, 8, 0x80); // set to IETF variant return QUuid::fromRfc4122(digest); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index a82d3f134..24608701d 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -104,14 +104,14 @@ class MinecraftAccount : public QObject, public Usable { public: /* queries */ QString internalId() const { return data.internalId; } - QString accountDisplayString() const { return data.accountDisplayString(); } - QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } QString profileName() const { return data.profileName(); } + QString displayName() const; + bool isActive() const; AccountType accountType() const noexcept { return data.type; } @@ -135,7 +135,7 @@ class MinecraftAccount : public QObject, public Usable { } } - QPixmap getFace() const; + QPixmap getFace(int width = 64, int height = 64) const; //! Returns the current state of the account AccountState accountState() const; diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index 11219bf17..1a4e9aa74 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -23,36 +23,35 @@ QString EntitlementsStep::describe() void EntitlementsStep::perform() { - auto uuid = QUuid::createUuid(); - m_entitlements_request_id = uuid.toString().remove('{').remove('}'); + m_entitlements_request_id = QUuid::createUuid().toString(QUuid::WithoutBraces); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Download::makeByteArray(url); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting entitlements..."; } -void EntitlementsStep::onRequestDone() +void EntitlementsStep::onRequestDone(QByteArray* response) { - qCDebug(authCredentials()) << *m_response; + qCDebug(authCredentials()) << *response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? - Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement); + Parsers::parseMinecraftEntitlements(*response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h index f20fcac08..72f77dabe 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.h +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -18,11 +17,10 @@ class EntitlementsStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: QString m_entitlements_request_id; - std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index b9e1dbcdb..7b26ca468 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -16,22 +16,22 @@ void GetSkinStep::perform() { QUrl url(m_data->minecraftProfile.skin.url); - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); + auto [request, response] = Net::Download::makeByteArray(url); + m_request = request; m_request->enableAutoRetry(true); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); } -void GetSkinStep::onRequestDone() +void GetSkinStep::onRequestDone(QByteArray* response) { if (m_request->error() == QNetworkReply::NoError) - m_data->minecraftProfile.skin.data = *m_response; + m_data->minecraftProfile.skin.data = *response; emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h index c598f05d9..2cd74ab92 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.h +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -18,10 +17,9 @@ class GetSkinStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index f91c2c595..4ceed8586 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -36,36 +36,37 @@ void LauncherLoginStep::perform() { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, requestBody.toUtf8()); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting Minecraft access token..."; } -void LauncherLoginStep::onRequestDone() +void LauncherLoginStep::onRequestDone(QByteArray* response) { - qCDebug(authCredentials()) << *m_response; + qCDebug(authCredentials()) << *response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); - if (Net::isApplicationError(m_request->error())) { + if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { + m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } return; } - if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) { + if (!Parsers::parseMojangResponse(*response, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h index 0b5969f2b..2501f5707 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.h +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" @@ -18,10 +17,9 @@ class LauncherLoginStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index aaf527cd4..78762a32a 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -66,16 +66,16 @@ void MSADeviceCodeStep::perform() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, payload); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); + connect(m_task.get(), &Task::finished, this, [this, response] { deviceAuthorizationFinished(response); }); m_task->start(); } @@ -111,21 +111,21 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d }; } -void MSADeviceCodeStep::deviceAuthorizationFinished() +void MSADeviceCodeStep::deviceAuthorizationFinished(QByteArray* response) { - auto rsp = parseDeviceAuthorizationResponse(*m_response); + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { + qWarning() << "Device authorization failed:" << m_request->error() << m_request->errorString(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(m_request->errorString())); + return; + } + + auto rsp = parseDeviceAuthorizationResponse(*response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } - if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { - qWarning() << "Device authorization failed:" << *m_response; - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); - 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")); @@ -182,11 +182,11 @@ void MSADeviceCodeStep::authenticateUser() { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, payload); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); - connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); + connect(m_request.get(), &Task::finished, this, [this, response] { authenticationFinished(response); }); m_request->setNetwork(APPLICATION->network()); m_request->start(); @@ -227,7 +227,7 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) obj.toVariantMap() }; } -void MSADeviceCodeStep::authenticationFinished() +void MSADeviceCodeStep::authenticationFinished(QByteArray* response) { if (m_request->error() == QNetworkReply::TimeoutError) { // rfc8628#section-3.5 @@ -239,7 +239,7 @@ void MSADeviceCodeStep::authenticationFinished() startPoolTimer(); return; } - auto rsp = parseAuthenticationResponse(*m_response); + auto rsp = parseAuthenticationResponse(*response); if (rsp.error == "slow_down") { // rfc8628#section-3.5 // "A variant of 'authorization_pending', the authorization request is diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index 7f755563f..cfb8270d4 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -58,10 +58,10 @@ class MSADeviceCodeStep : public AuthStep { void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: - void deviceAuthorizationFinished(); + void deviceAuthorizationFinished(QByteArray* response); void startPoolTimer(); void authenticateUser(); - void authenticationFinished(); + void authenticationFinished(QByteArray* response); private: QString m_clientId; @@ -72,7 +72,6 @@ class MSADeviceCodeStep : public AuthStep { QTimer m_pool_timer; QTimer m_expiration_timer; - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 29bab69d5..51a5e5ce0 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include @@ -135,7 +136,7 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile m_oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); m_oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); m_oauth2.setClientIdentifier(m_clientId); - m_oauth2.setNetworkAccessManager(APPLICATION->network().get()); + m_oauth2.setNetworkAccessManager(APPLICATION->network()); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { m_data->msaClientID = m_oauth2.clientIdentifier(); diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index e8507f88a..b95682b2b 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -21,21 +21,21 @@ void MinecraftProfileStep::perform() { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Download::makeByteArray(url); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); } -void MinecraftProfileStep::onRequestDone() +void MinecraftProfileStep::onRequestDone(QByteArray* response) { if (m_request->error() == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. @@ -50,18 +50,19 @@ void MinecraftProfileStep::onRequestDone() qWarning() << " Error string :" << m_request->errorString(); qWarning() << " Response:"; - qWarning() << QString::fromUtf8(*m_response); + qWarning() << QString::fromUtf8(*response); - if (Net::isApplicationError(m_request->error())) { + if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } else { + m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } return; } - if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) { + if (!Parsers::parseMinecraftProfile(*response, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); return; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h index e8b35b875..5348f5ba1 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.h +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" @@ -18,10 +17,9 @@ class MinecraftProfileStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 01d96c127..422aa1a54 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include "Application.h" #include "Logging.h" @@ -12,7 +12,7 @@ #include "net/Upload.h" XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind) - : AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind) + : AuthStep(data), m_token(token), m_relyingParty(std::move(relyingParty)), m_authorizationKind(std::move(authorizationKind)) {} QString XboxAuthorizationStep::describe() @@ -22,7 +22,7 @@ QString XboxAuthorizationStep::describe() void XboxAuthorizationStep::perform() { - QString xbox_auth_template = R"XXX( + const QString xboxAuthTemplate = R"XXX( { "Properties": { "SandboxId": "RETAIL", @@ -34,42 +34,40 @@ void XboxAuthorizationStep::perform() "TokenType": "JWT" } )XXX"; - auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); + const auto xboxAuthData = xboxAuthTemplate.arg(m_data->userToken.token, m_relyingParty); // http://xboxlive.com - QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); - auto headers = QList{ - { "Content-Type", "application/json" }, - { "Accept", "application/json" }, - }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + const QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); + auto headers = QList{ { .headerName = "Content-Type", .headerValue = "application/json" }, + { .headerName = "Accept", .headerValue = "application/json" }, + { .headerName = "x-xbl-contract-version", .headerValue = "1" } }; + auto [request, response] = Net::Upload::makeByteArray(url, xboxAuthData.toUtf8()); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "Getting authorization token for" << m_relyingParty; } -void XboxAuthorizationStep::onRequestDone() +void XboxAuthorizationStep::onRequestDone(QByteArray* response) { - qCDebug(authCredentials()) << *m_response; + qCDebug(authCredentials()) << *response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); - if (Net::isApplicationError(m_request->error())) { - if (!processSTSError()) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_request->error())); - } else { - emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); + if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { + if (processSTSError(*response)) { + return; } + emit finished(AccountTaskState::STATE_FAILED_SOFT, + tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } else { + m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } @@ -77,7 +75,7 @@ void XboxAuthorizationStep::onRequestDone() } Token temp; - if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) { + if (!Parsers::parseXTokenResponse(*response, temp, m_authorizationKind)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)); return; @@ -94,12 +92,12 @@ void XboxAuthorizationStep::onRequestDone() emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); } -bool XboxAuthorizationStep::processSTSError() +bool XboxAuthorizationStep::processSTSError(const QByteArray& response) { if (m_request->error() == QNetworkReply::AuthenticationRequiredError) { QJsonParseError jsonError; - QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError); - if (jsonError.error) { + const QJsonDocument doc = QJsonDocument::fromJson(response, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { qWarning() << "Cannot parse error XSTS response as JSON:" << jsonError.errorString(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString())); diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h index 8418727c4..9f424c0c3 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" @@ -18,17 +17,16 @@ class XboxAuthorizationStep : public AuthStep { QString describe() override; private: - bool processSTSError(); + bool processSTSError(const QByteArray& response); private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: Token* m_token; QString m_relyingParty; QString m_authorizationKind; - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp deleted file mode 100644 index ef127caca..000000000 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "XboxProfileStep.h" - -#include -#include - -#include "Application.h" -#include "Logging.h" -#include "net/NetUtils.h" -#include "net/RawHeaderProxy.h" - -XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {} - -QString XboxProfileStep::describe() -{ - return tr("Fetching Xbox profile."); -} - -void XboxProfileStep::perform() -{ - QUrl url("https://profile.xboxlive.com/users/me/profile/settings"); - QUrlQuery q; - q.addQueryItem("settings", - "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," - "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," - "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," - "PreferredColor,Location,Bio,Watermarks," - "RealName,RealNameOverride,IsQuarantined"); - url.setQuery(q); - auto headers = QList{ - { "Content-Type", "application/json" }, - { "Accept", "application/json" }, - { "x-xbl-contract-version", "3" }, - { "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() } - }; - - m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); - - m_task.reset(new NetJob("XboxProfileStep", APPLICATION->network())); - m_task->setAskRetry(false); - m_task->addNetAction(m_request); - - connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone); - - m_task->start(); - qDebug() << "Getting Xbox profile..."; -} - -void XboxProfileStep::onRequestDone() -{ - if (m_request->error() != QNetworkReply::NoError) { - qWarning() << "Reply error:" << m_request->error(); - qCDebug(authCredentials()) << *m_response; - if (Net::isApplicationError(m_request->error())) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); - } else { - emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); - } - return; - } - - qCDebug(authCredentials()) << "Xbox profile:" << *m_response; - - emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); -} diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h deleted file mode 100644 index f2ab874f2..000000000 --- a/launcher/minecraft/auth/steps/XboxProfileStep.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include -#include - -#include "minecraft/auth/AuthStep.h" -#include "net/Download.h" -#include "net/NetJob.h" - -class XboxProfileStep : public AuthStep { - Q_OBJECT - - public: - explicit XboxProfileStep(AccountData* data); - virtual ~XboxProfileStep() noexcept = default; - - void perform() override; - - QString describe() override; - - private slots: - void onRequestDone(); - - private: - std::shared_ptr m_response; - Net::Download::Ptr m_request; - NetJob::Ptr m_task; -}; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index 64c2dd340..382750218 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -37,35 +37,36 @@ void XboxUserStep::perform() // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders { "x-xbl-contract-version", "1" } }; - m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [request, response] = Net::Upload::makeByteArray(url, xbox_auth_data.toUtf8()); + m_request = request; + m_request->addHeaderProxy(std::make_unique(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); - connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone); + connect(m_task.get(), &Task::finished, this, [this, response] { onRequestDone(response); }); m_task->start(); qDebug() << "First layer of Xbox auth ... commencing."; } -void XboxUserStep::onRequestDone() +void XboxUserStep::onRequestDone(QByteArray* response) { if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); - if (Net::isApplicationError(m_request->error())) { + if (Net::isApplicationError(m_request->error()) && !Net::isServerError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } else { + m_data->networkError = m_request->error(); emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } return; } Token temp; - if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) { + if (!Parsers::parseXTokenResponse(*response, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication response could not be understood.")); return; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h index f6cc822f2..b6330a499 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.h +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" @@ -18,10 +17,9 @@ class XboxUserStep : public AuthStep { QString describe() override; private slots: - void onRequestDone(); + void onRequestDone(QByteArray* response); private: - std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index 6a2bcb311..f60780f1b 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -244,7 +244,7 @@ bool AutoInstallJava::abort() { if (m_current_task && m_current_task->canAbort()) { auto status = m_current_task->abort(); - emitFailed("Aborted."); + emitAborted(); return status; } return Task::abort(); diff --git a/launcher/minecraft/launch/AutoInstallJava.h b/launcher/minecraft/launch/AutoInstallJava.h index cbfcf5ee7..a4ffdff29 100644 --- a/launcher/minecraft/launch/AutoInstallJava.h +++ b/launcher/minecraft/launch/AutoInstallJava.h @@ -59,7 +59,7 @@ class AutoInstallJava : public LaunchStep { void tryNextMajorJava(); private: - MinecraftInstancePtr m_instance; + MinecraftInstance* m_instance; Task::Ptr m_current_task; qsizetype m_majorJavaVersionIndex = 0; diff --git a/launcher/minecraft/launch/ClaimAccount.cpp b/launcher/minecraft/launch/ClaimAccount.cpp index a3de1516a..1b375abb3 100644 --- a/launcher/minecraft/launch/ClaimAccount.cpp +++ b/launcher/minecraft/launch/ClaimAccount.cpp @@ -6,7 +6,7 @@ ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session) : LaunchStep(parent) { - if (session->status == AuthSession::Status::PlayableOnline && !session->demo) { + if (session->launchMode == LaunchMode::Normal) { auto accounts = APPLICATION->accounts(); m_account = accounts->getAccountByProfileName(session->player_name); } @@ -15,9 +15,9 @@ ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session) : LaunchS void ClaimAccount::executeTask() { if (m_account) { - lock.reset(new UseLock(m_account)); - emitSucceeded(); + lock.reset(new UseLock(m_account.get())); } + emitSucceeded(); } void ClaimAccount::finalize() diff --git a/launcher/minecraft/launch/EnsureAvailableMemory.cpp b/launcher/minecraft/launch/EnsureAvailableMemory.cpp new file mode 100644 index 000000000..0f349674a --- /dev/null +++ b/launcher/minecraft/launch/EnsureAvailableMemory.cpp @@ -0,0 +1,113 @@ +// 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 "EnsureAvailableMemory.h" + +#include "HardwareInfo.h" +#include "ui/dialogs/CustomMessageBox.h" + +EnsureAvailableMemory::EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance) : LaunchStep(parent), m_instance(instance) {} + +void EnsureAvailableMemory::executeTask() +{ +#ifdef Q_OS_MACOS + QString text; + switch (MacOSHardwareInfo::memoryPressureLevel()) { + case MacOSHardwareInfo::MemoryPressureLevel::Normal: + emitSucceeded(); + return; + case MacOSHardwareInfo::MemoryPressureLevel::Warning: + text = + tr("The system is under increased memory pressure.\n" + "This may lead to lag or slowdowns.\n" + "If possible, close other applications before continuing.\n\n" + "Launch anyway?"); + break; + case MacOSHardwareInfo::MemoryPressureLevel::Critical: + text = + tr("Your system is under critical memory pressure.\n" + "This may lead to severe slowdowns, crashes or system instability.\n" + "It is recommended to close other applications or restart your system.\n\n" + "Launch anyway?"); + break; + } + + bool shouldAbort = false; + + if (m_instance->settings()->get("LowMemWarning").toBool()) { + auto* dialog = CustomMessageBox::selectable(nullptr, tr("High memory pressure"), text, QMessageBox::Icon::Warning, + QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, + QMessageBox::StandardButton::No); + + shouldAbort = dialog->exec() == QMessageBox::No; + dialog->deleteLater(); + } + + const auto message = tr("The system is under high memory pressure"); + if (shouldAbort) { + emit logLine(message, MessageLevel::Fatal); + emitFailed(message); + return; + } + + emit logLine(message, MessageLevel::Warning); + emitSucceeded(); +#else + const uint64_t available = HardwareInfo::availableRamMiB(); + if (available == 0) { + // could not read + emitSucceeded(); + return; + } + + const uint64_t settingMin = m_instance->settings()->get("MinMemAlloc").toUInt(); + const uint64_t settingMax = m_instance->settings()->get("MaxMemAlloc").toUInt(); + const uint64_t max = std::max(settingMin, settingMax); + + if (static_cast(max) * 0.9 > static_cast(available)) { + bool shouldAbort = false; + + if (m_instance->settings()->get("LowMemWarning").toBool()) { + auto* dialog = CustomMessageBox::selectable( + nullptr, tr("Low free memory"), + tr("There might not be enough free RAM to launch this instance with the current memory settings.\n\n" + "Maximum allocated: %1 MiB\nFree: %2 MiB (out of %3 MiB total)\n\n" + "Launch anyway? This may cause slowdowns in the game and your system.") + .arg(max) + .arg(available) + .arg(HardwareInfo::totalRamMiB()), + QMessageBox::Icon::Warning, QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, + QMessageBox::StandardButton::No); + + shouldAbort = dialog->exec() == QMessageBox::No; + dialog->deleteLater(); + } + + const auto message = tr("Not enough RAM available to launch this instance"); + if (shouldAbort) { + emit logLine(message, MessageLevel::Fatal); + emitFailed(message); + return; + } + + emit logLine(message, MessageLevel::Warning); + } + + emitSucceeded(); +#endif +} diff --git a/launcher/minecraft/launch/EnsureAvailableMemory.h b/launcher/minecraft/launch/EnsureAvailableMemory.h new file mode 100644 index 000000000..3074a3f9a --- /dev/null +++ b/launcher/minecraft/launch/EnsureAvailableMemory.h @@ -0,0 +1,36 @@ +// 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 "launch/LaunchStep.h" +#include "minecraft/MinecraftInstance.h" + +class EnsureAvailableMemory : public LaunchStep { + Q_OBJECT + + public: + explicit EnsureAvailableMemory(LaunchTask* parent, MinecraftInstance* instance); + ~EnsureAvailableMemory() override = default; + + void executeTask() override; + bool canAbort() const override { return false; } + + private: + MinecraftInstance* m_instance; +}; diff --git a/launcher/minecraft/launch/EnsureOfflineLibraries.cpp b/launcher/minecraft/launch/EnsureOfflineLibraries.cpp index 59801a1d0..0165fbdf9 100644 --- a/launcher/minecraft/launch/EnsureOfflineLibraries.cpp +++ b/launcher/minecraft/launch/EnsureOfflineLibraries.cpp @@ -27,16 +27,27 @@ void EnsureOfflineLibraries::executeTask() { const auto profile = m_instance->getPackProfile()->getProfile(); QStringList allJars; - profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot()); + profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot(), + false); + + QStringList missing; for (const auto& jar : allJars) { if (!QFileInfo::exists(jar)) { - emit logLine(tr("This instance cannot be launched because some libraries are missing or have not been downloaded yet. Please " - "try again in online mode with a working Internet connection"), - MessageLevel::Fatal); - emitFailed("Required libraries are missing"); - return; + missing.append(jar); } } - emitSucceeded(); + if (missing.isEmpty()) { + emitSucceeded(); + return; + } + + emit logLine("Missing libraries:", MessageLevel::Error); + for (const auto& jar : missing) { + emit logLine(" " + jar, MessageLevel::Error); + } + emit logLine(tr("\nThis instance cannot be launched because some libraries are missing or have not been downloaded yet. Please " + "try again in online mode with a working Internet connection"), + MessageLevel::Fatal); + emitFailed("Required libraries are missing"); } diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index 6d54d9b6a..17d1a8cda 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -53,7 +53,7 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); - return f->writeFile(ext, absFilePath); + return f->writeFile(ext, absFilePath, directory); }); } @@ -65,7 +65,6 @@ void ExtractNatives::executeTask() emitSucceeded(); return; } - auto settings = instance->settings(); auto outputPath = instance->getNativePath(); FS::ensureFolderPathExists(outputPath); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index d966287af..e4d3ec1ef 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -94,8 +94,8 @@ void LauncherPartLaunch::executeTask() m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); QStringList args = instance->javaArguments(); - QString allArgs = args.join(", "); - emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); + QString allArgs = args.join(" "); + emit logLine("Java arguments:\n " + m_parent->censorPrivateInfo(allArgs) + "\n", MessageLevel::Launcher); auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); @@ -164,9 +164,9 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) switch (state) { case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - const char* reason = QT_TR_NOOP("Could not launch Minecraft!"); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(tr(reason)); + const char* reason = QT_TR_NOOP("Could not launch Minecraft: %1"); + emit logLine(QString(reason).arg(m_process.errorString()), MessageLevel::Fatal); + emitFailed(tr(reason).arg(m_process.errorString())); return; } case LoggedProcess::Aborted: diff --git a/launcher/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp index e44d09839..df8c28c7b 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.cpp +++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -19,49 +19,10 @@ #include #include "PrintInstanceInfo.h" -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#include "HardwareInfo.h" + +#if defined(Q_OS_FREEBSD) namespace { -#if defined(Q_OS_LINUX) -void probeProcCpuinfo(QStringList& log) -{ - std::ifstream cpuin("/proc/cpuinfo"); - for (std::string line; std::getline(cpuin, line);) { - if (strncmp(line.c_str(), "model name", 10) == 0) { - log << QString::fromStdString(line.substr(13, std::string::npos)); - break; - } - } -} - -void runLspci(QStringList& log) -{ - // FIXME: fixed size buffers... - char buff[512]; - int gpuline = -1; - int cline = 0; - FILE* lspci = popen("lspci -k", "r"); - - if (!lspci) - return; - - while (fgets(buff, 512, lspci) != NULL) { - std::string str(buff); - if (str.length() < 9) - continue; - if (str.substr(8, 3) == "VGA") { - gpuline = cline; - log << QString::fromStdString(str.substr(35, std::string::npos)); - } - if (gpuline > -1 && gpuline != cline) { - if (cline - gpuline < 3) { - log << QString::fromStdString(str.substr(1, std::string::npos)); - } - } - cline++; - } - pclose(lspci); -} -#elif defined(Q_OS_FREEBSD) void runSysctlHwModel(QStringList& log) { char buff[512]; @@ -92,24 +53,6 @@ void runPciconf(QStringList& log) } pclose(pciconf); } -#endif -void runGlxinfo(QStringList& log) -{ - // FIXME: fixed size buffers... - char buff[512]; - FILE* glxinfo = popen("glxinfo", "r"); - if (!glxinfo) - return; - - while (fgets(buff, 512, glxinfo) != NULL) { - if (strncmp(buff, "OpenGL version string:", 22) == 0) { - log << QString::fromUtf8(buff); - break; - } - } - pclose(glxinfo); -} - } // namespace #endif @@ -118,16 +61,23 @@ void PrintInstanceInfo::executeTask() auto instance = m_parent->instance(); QStringList log; -#if defined(Q_OS_LINUX) - ::probeProcCpuinfo(log); - ::runLspci(log); - ::runGlxinfo(log); -#elif defined(Q_OS_FREEBSD) + log << ""; + log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion()); +#ifdef Q_OS_FREEBSD ::runSysctlHwModel(log); ::runPciconf(log); - ::runGlxinfo(log); +#else + log << "CPU: " + HardwareInfo::cpuInfo(); +#ifdef Q_OS_MACOS + log << "Memory pressure level: " + MacOSHardwareInfo::memoryPressureLevelName(); +#else + log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB()); #endif +#endif + log.append(HardwareInfo::gpuInfo()); + log << ""; + logLines(log, MessageLevel::Launcher); logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); emitSucceeded(); diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index 1a2ddf194..cbe1599cb 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -45,19 +45,19 @@ void ScanModFolders::executeTask() auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); - connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); if (!loaders->update()) { m_modsDone = true; } auto cores = m_inst->coreModList(); - connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); if (!cores->update()) { m_coreModsDone = true; } auto nils = m_inst->nilModList(); - connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); if (!nils->update()) { m_nilModsDone = true; } diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index bc950d673..9a1d14433 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -71,7 +71,7 @@ void VerifyJavaInstall::executeTask() } if (ignoreCompatibility) { - emit logLine(tr("Java major version is incompatible. Things might break."), MessageLevel::Warning); + emit logLine(tr("Java major version is incompatible. Things might break.\n"), MessageLevel::Warning); emitSucceeded(); return; } diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index e089f2f6e..c2b477d45 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "MTPixmapCache.h" #include "Version.h" @@ -31,61 +32,121 @@ // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats -static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } }, - { 5, { Version("1.15"), Version("1.16.1") } }, - { 6, { Version("1.16.2"), Version("1.16.5") } }, - { 7, { Version("1.17"), Version("1.17.1") } }, - { 8, { Version("1.18"), Version("1.18.1") } }, - { 9, { Version("1.18.2"), Version("1.18.2") } }, - { 10, { Version("1.19"), Version("1.19.3") } }, - { 11, { Version("23w03a"), Version("23w05a") } }, - { 12, { Version("1.19.4"), Version("1.19.4") } }, - { 13, { Version("23w12a"), Version("23w14a") } }, - { 14, { Version("23w16a"), Version("23w17a") } }, - { 15, { Version("1.20"), Version("1.20.1") } }, - { 16, { Version("23w31a"), Version("23w31a") } }, - { 17, { Version("23w32a"), Version("23w35a") } }, - { 18, { Version("1.20.2"), Version("1.20.2") } }, - { 19, { Version("23w40a"), Version("23w40a") } }, - { 20, { Version("23w41a"), Version("23w41a") } }, - { 21, { Version("23w42a"), Version("23w42a") } }, - { 22, { Version("23w43a"), Version("23w43b") } }, - { 23, { Version("23w44a"), Version("23w44a") } }, - { 24, { Version("23w45a"), Version("23w45a") } }, - { 25, { Version("23w46a"), Version("23w46a") } }, - { 26, { Version("1.20.3"), Version("1.20.4") } }, - { 27, { Version("23w51a"), Version("23w51b") } }, - { 28, { Version("24w05a"), Version("24w05b") } }, - { 29, { Version("24w04a"), Version("24w04a") } }, - { 30, { Version("24w05a"), Version("24w05b") } }, - { 31, { Version("24w06a"), Version("24w06a") } }, - { 32, { Version("24w07a"), Version("24w07a") } }, - { 33, { Version("24w09a"), Version("24w09a") } }, - { 34, { Version("24w10a"), Version("24w10a") } }, - { 35, { Version("24w11a"), Version("24w11a") } }, - { 36, { Version("24w12a"), Version("24w12a") } }, - { 37, { Version("24w13a"), Version("24w13a") } }, - { 38, { Version("24w14a"), Version("24w14a") } }, - { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, - { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, - { 41, { Version("1.20.5"), Version("1.20.6") } }, - { 42, { Version("24w18a"), Version("24w18a") } }, - { 43, { Version("24w19a"), Version("24w19b") } }, - { 44, { Version("24w20a"), Version("24w20a") } }, - { 45, { Version("21w21a"), Version("21w21b") } }, - { 46, { Version("1.21-pre1"), Version("1.21-pre1") } }, - { 47, { Version("1.21-pre2"), Version("1.21-pre2") } }, - { 48, { Version("1.21"), Version("1.21") } } }; +static const QMap, std::pair> s_pack_format_versions = { + { { 4, 0 }, { Version("1.13"), Version("1.14.4") } }, + { { 5, 0 }, { Version("1.15"), Version("1.16.1") } }, + { { 6, 0 }, { Version("1.16.2"), Version("1.16.5") } }, + { { 7, 0 }, { Version("1.17"), Version("1.17.1") } }, + { { 8, 0 }, { Version("1.18"), Version("1.18.1") } }, + { { 9, 0 }, { Version("1.18.2"), Version("1.18.2") } }, + { { 10, 0 }, { Version("1.19"), Version("1.19.3") } }, + { { 11, 0 }, { Version("23w03a"), Version("23w05a") } }, + { { 12, 0 }, { Version("1.19.4"), Version("1.19.4") } }, + { { 13, 0 }, { Version("23w12a"), Version("23w14a") } }, + { { 14, 0 }, { Version("23w16a"), Version("23w17a") } }, + { { 15, 0 }, { Version("1.20"), Version("1.20.1") } }, + { { 16, 0 }, { Version("23w31a"), Version("23w31a") } }, + { { 17, 0 }, { Version("23w32a"), Version("23w35a") } }, + { { 18, 0 }, { Version("1.20.2"), Version("1.20.2") } }, + { { 19, 0 }, { Version("23w40a"), Version("23w40a") } }, + { { 20, 0 }, { Version("23w41a"), Version("23w41a") } }, + { { 21, 0 }, { Version("23w42a"), Version("23w42a") } }, + { { 22, 0 }, { Version("23w43a"), Version("23w43b") } }, + { { 23, 0 }, { Version("23w44a"), Version("23w44a") } }, + { { 24, 0 }, { Version("23w45a"), Version("23w45a") } }, + { { 25, 0 }, { Version("23w46a"), Version("23w46a") } }, + { { 26, 0 }, { Version("1.20.3"), Version("1.20.4") } }, + { { 27, 0 }, { Version("23w51a"), Version("23w51b") } }, + { { 28, 0 }, { Version("24w03a"), Version("24w03b") } }, + { { 29, 0 }, { Version("24w04a"), Version("24w04a") } }, + { { 30, 0 }, { Version("24w05a"), Version("24w05b") } }, + { { 31, 0 }, { Version("24w06a"), Version("24w06a") } }, + { { 32, 0 }, { Version("24w07a"), Version("24w07a") } }, + { { 33, 0 }, { Version("24w09a"), Version("24w09a") } }, + { { 34, 0 }, { Version("24w10a"), Version("24w10a") } }, + { { 35, 0 }, { Version("24w11a"), Version("24w11a") } }, + { { 36, 0 }, { Version("24w12a"), Version("24w12a") } }, + { { 37, 0 }, { Version("24w13a"), Version("24w13a") } }, + { { 38, 0 }, { Version("24w14a"), Version("24w14a") } }, + { { 39, 0 }, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, + { { 40, 0 }, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, + { { 41, 0 }, { Version("1.20.5"), Version("1.20.6") } }, + { { 42, 0 }, { Version("24w18a"), Version("24w18a") } }, + { { 43, 0 }, { Version("24w19a"), Version("24w19b") } }, + { { 44, 0 }, { Version("24w20a"), Version("24w20a") } }, + { { 45, 0 }, { Version("24w21a"), Version("24w21b") } }, + { { 46, 0 }, { Version("1.21-pre1"), Version("1.21-pre1") } }, + { { 47, 0 }, { Version("1.21-pre2"), Version("1.21-pre2") } }, + { { 48, 0 }, { Version("1.21"), Version("1.21.1") } }, + { { 49, 0 }, { Version("24w33a"), Version("24w33a") } }, + { { 50, 0 }, { Version("24w34a"), Version("24w34a") } }, + { { 51, 0 }, { Version("24w35a"), Version("24w35a") } }, + { { 52, 0 }, { Version("24w36a"), Version("24w36a") } }, + { { 53, 0 }, { Version("24w37a"), Version("24w37a") } }, + { { 54, 0 }, { Version("24w38a"), Version("24w38a") } }, + { { 55, 0 }, { Version("24w39a"), Version("24w39a") } }, + { { 56, 0 }, { Version("24w40a"), Version("24w40a") } }, + { { 57, 0 }, { Version("1.21.2"), Version("1.21.3") } }, + { { 58, 0 }, { Version("24w44a"), Version("24w44a") } }, + { { 59, 0 }, { Version("24w45a"), Version("24w45a") } }, + { { 60, 0 }, { Version("24w46a"), Version("1.21.4-pre1") } }, + { { 61, 0 }, { Version("1.21.4"), Version("1.21.4") } }, + { { 62, 0 }, { Version("25w02a"), Version("25w02a") } }, + { { 63, 0 }, { Version("25w03a"), Version("25w03a") } }, + { { 64, 0 }, { Version("25w04a"), Version("25w04a") } }, + { { 65, 0 }, { Version("25w05a"), Version("25w05a") } }, + { { 66, 0 }, { Version("25w06a"), Version("25w06a") } }, + { { 67, 0 }, { Version("25w07a"), Version("25w07a") } }, + { { 68, 0 }, { Version("25w08a"), Version("25w08a") } }, + { { 69, 0 }, { Version("25w09a"), Version("25w09b") } }, + { { 70, 0 }, { Version("25w10a"), Version("1.21.5-pre1") } }, + { { 71, 0 }, { Version("1.21.5"), Version("1.21.5") } }, + { { 72, 0 }, { Version("25w15a"), Version("25w15a") } }, + { { 73, 0 }, { Version("25w16a"), Version("25w16a") } }, + { { 74, 0 }, { Version("25w17a"), Version("25w17a") } }, + { { 75, 0 }, { Version("25w18a"), Version("25w18a") } }, + { { 76, 0 }, { Version("25w19a"), Version("25w19a") } }, + { { 77, 0 }, { Version("25w20a"), Version("25w20a") } }, + { { 78, 0 }, { Version("25w21a"), Version("25w21a") } }, + { { 79, 0 }, { Version("1.21.6-pre1"), Version("1.21.6-pre2") } }, + { { 80, 0 }, { Version("1.21.6"), Version("1.21.6") } }, + { { 81, 0 }, { Version("1.21.7"), Version("1.21.8") } }, + { { 82, 0 }, { Version("25w31a"), Version("25w31a") } }, + { { 83, 0 }, { Version("25w32a"), Version("25w32a") } }, + { { 83, 1 }, { Version("25w33a"), Version("25w33a") } }, + { { 84, 0 }, { Version("25w34a"), Version("25w34b") } }, + { { 85, 0 }, { Version("25w35a"), Version("25w35a") } }, + { { 86, 0 }, { Version("25w36a"), Version("25w36b") } }, + { { 87, 0 }, { Version("25w37a"), Version("1.21.9-pre1") } }, + { { 87, 1 }, { Version("1.21.9-pre1"), Version("1.21.9-pre1") } }, + { { 88, 0 }, { Version("1.21.9"), Version("1.21.10") } }, + { { 89, 0 }, { Version("25w41a"), Version("25w41a") } }, + { { 90, 0 }, { Version("25w42a"), Version("25w42a") } }, + { { 91, 0 }, { Version("25w43a"), Version("25w43a") } }, + { { 92, 0 }, { Version("25w44a"), Version("25w44a") } }, + { { 93, 0 }, { Version("25w45a"), Version("25w45a") } }, + { { 93, 1 }, { Version("25w46a"), Version("25w46a") } }, + { { 94, 0 }, { Version("1.21.11-pre1"), Version("1.21.11-pre3") } }, + { { 94, 1 }, { Version("1.21.11-pre4"), Version("1.21.11") } }, + { { 95, 0 }, { Version("26.1-snap1"), Version("26.1-snap1") } }, + { { 96, 0 }, { Version("26.1-snap2"), Version("26.1-snap2") } }, + { { 97, 0 }, { Version("26.1-snap3"), Version("26.1-snap3") } }, + { { 97, 1 }, { Version("26.1-snap4"), Version("26.1-snap4") } }, + { { 98, 0 }, { Version("26.1-snap5"), Version("26.1-snap5") } }, + { { 99, 0 }, { Version("26.1-snap6"), Version("26.1-snap6") } }, + { { 99, 1 }, { Version("26.1-snap7"), Version("26.1-snap7") } }, + { { 99, 2 }, { Version("26.1-snap8"), Version("26.1-snap9") } }, + { { 99, 3 }, { Version("26.1-snap10"), Version("26.1-snap10") } }, + { { 100, 0 }, { Version("26.1-snap11"), Version("26.1-snap11") } }, +}; -void DataPack::setPackFormat(int new_format_id) +void DataPack::setPackFormat(int new_format_id, std::pair min_format, std::pair max_format) { QMutexLocker locker(&m_data_lock); - if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; - } - m_pack_format = new_format_id; + m_min_format = min_format; + m_max_format = max_format; } void DataPack::setDescription(QString new_description) @@ -140,19 +201,22 @@ QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const return image(size); } -std::pair DataPack::compatibleVersions() const +static std::pair map(std::pair format, const QMap, std::pair>& versions) { - if (!s_pack_format_versions.contains(m_pack_format)) { + if (format.first == 0 || !versions.contains(format)) { return { {}, {} }; } - - return s_pack_format_versions.constFind(m_pack_format).value(); + return versions.constFind(format).value(); +} +static std::pair map(int format, const QMap, std::pair>& versions) +{ + return map({ format, 0 }, versions); } int DataPack::compare(const Resource& other, SortType type) const { - auto const& cast_other = static_cast(other); - if (type == SortType::PACK_FORMAT) { + const auto& cast_other = static_cast(other); + if (type == SortType::PackFormat) { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); @@ -168,21 +232,64 @@ int DataPack::compare(const Resource& other, SortType type) const bool DataPack::applyFilter(QRegularExpression filter) const { - if (filter.match(description()).hasMatch()) + if (filter.match(description()).hasMatch()) { return true; + } - if (filter.match(QString::number(packFormat())).hasMatch()) - return true; - - if (filter.match(compatibleVersions().first.toString()).hasMatch()) - return true; - if (filter.match(compatibleVersions().second.toString()).hasMatch()) + if (filter.match(QString::number(packFormat())).hasMatch()) { return true; + } + auto versions = { map(m_pack_format, mappings()), map(m_min_format, mappings()), map(m_max_format, mappings()) }; + for (const auto& version : versions) { + if (!version.first.isEmpty()) { + if (filter.match(version.first.toString()).hasMatch()) { + return true; + } + if (filter.match(version.second.toString()).hasMatch()) { + return true; + } + } + } return Resource::applyFilter(filter); } bool DataPack::valid() const { - return m_pack_format != 0; + return m_pack_format != 0 || (m_min_format.first != 0 && m_max_format.first != 0); +} + +QMap, std::pair> DataPack::mappings() const +{ + return s_pack_format_versions; +} + +QString DataPack::packFormatStr() const +{ + if (m_pack_format != 0) { + auto version_bounds = map(m_pack_format, mappings()); + if (version_bounds.first.toString().isEmpty()) { + return QString::number(m_pack_format); + } + return QString("%1 (%2 - %3)") + .arg(QString::number(m_pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + auto min_bound = map(m_min_format, mappings()); + auto max_bound = map(m_max_format, mappings()); + auto min_version = min_bound.first; + auto max_version = max_bound.second; + if (min_version.isEmpty() || max_version.isEmpty()) { + return tr("Unrecognized"); + } + auto str = QString("[") + QString::number(m_min_format.first); + if (m_min_format.second != 0) { + str += "." + QString::number(m_min_format.second); + } + + str += QString(" - ") + QString::number(m_max_format.first); + if (m_max_format.second != 0) { + str += "." + QString::number(m_max_format.second); + } + + return str + QString(" (%2 - %3)").arg(min_version.toString(), max_version.toString()); } diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 2943dd4bc..89da0178a 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -25,6 +25,7 @@ #include #include +#include class Version; @@ -41,8 +42,6 @@ class DataPack : public Resource { /** Gets the numerical ID of the pack format. */ int packFormat() const { return m_pack_format; } - /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - virtual std::pair compatibleVersions() const; /** Gets the description of the data pack. */ QString description() const { return m_description; } @@ -51,7 +50,7 @@ class DataPack : public Resource { QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ - void setPackFormat(int new_format_id); + void setPackFormat(int new_format_id, std::pair min_format, std::pair max_format); /** Thread-safe. */ void setDescription(QString new_description); @@ -61,9 +60,14 @@ class DataPack : public Resource { bool valid() const override; - [[nodiscard]] int compare(Resource const& other, SortType type) const override; + [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + QString packFormatStr() const; + + protected: + virtual QMap, std::pair> mappings() const; + protected: mutable QMutex m_data_lock; @@ -71,6 +75,8 @@ class DataPack : public Resource { * See https://minecraft.wiki/w/Data_pack#pack.mcmeta */ int m_pack_format = 0; + std::pair m_min_format; + std::pair m_max_format; /** The data pack's description, as defined in the pack.mcmeta file. */ diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp index 7abab7a06..350ae60e1 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.cpp +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -40,94 +40,95 @@ #include #include -#include "Version.h" - #include "minecraft/mod/tasks/LocalDataPackParseTask.h" -DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) - : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; + m_columnNames = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size", "File Name" }); + m_columnNamesTranslated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size"), tr("File Name") }); + m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::PackFormat, + SortType::Date, SortType::Size, SortType::Filename }; + m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const { - if (!validateIndex(index)) + if (!validateIndex(index)) { return {}; + } int row = index.row(); int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: - switch (column) { - case NameColumn: - return m_resources[row]->name(); - case PackFormatColumn: { - auto& resource = at(row); - auto pack_format = resource.packFormat(); - if (pack_format == 0) - return tr("Unrecognized"); - - auto version_bounds = resource.compatibleVersions(); - if (version_bounds.first.toString().isEmpty()) - return QString::number(pack_format); - - return QString("%1 (%2 - %3)") - .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); - } - case DateColumn: - return m_resources[row]->dateTimeChanged(); - - default: - return {}; + if (column == PackFormatColumn) { + const auto& resource = at(row); + return resource.packFormatStr(); } + if (column == SizeColumn) { + const auto& resource = at(row); + return resource.sizeStr(); + } + break; case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - else - return {}; + break; default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case FileNameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + default: + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const @@ -140,6 +141,8 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien case PackFormatColumn: case DateColumn: case ImageColumn: + case SizeColumn: + case FileNameColumn: return columnNames().at(section); default: return {}; @@ -156,6 +159,10 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this data pack was last changed (or added)."); + case SizeColumn: + return tr("The size of the data pack."); + case FileNameColumn: + return tr("The file name of the data pack."); default: return {}; } @@ -171,7 +178,7 @@ QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien int DataPackFolderModel::columnCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : NUM_COLUMNS; + return parent.isValid() ? 0 : NumColumns; } Resource* DataPackFolderModel::createResource(const QFileInfo& file) @@ -181,5 +188,5 @@ Resource* DataPackFolderModel::createResource(const QFileInfo& file) Task* DataPackFolderModel::createParseTask(Resource& resource) { - return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); + return new LocalDataPackParseTask(m_nextResolutionTicket, static_cast(&resource)); } diff --git a/launcher/minecraft/mod/DataPackFolderModel.h b/launcher/minecraft/mod/DataPackFolderModel.h index 2b90e1a2a..24133354a 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.h +++ b/launcher/minecraft/mod/DataPackFolderModel.h @@ -39,16 +39,24 @@ #include "ResourceFolderModel.h" #include "DataPack.h" -#include "ResourcePack.h" class DataPackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; + enum Columns : std::uint8_t { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + SizeColumn, + FileNameColumn, + NumColumns + }; - explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr); - virtual QString id() const override { return "datapacks"; } + QString id() const override { return "datapacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -56,7 +64,7 @@ class DataPackFolderModel : public ResourceFolderModel { int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override; - [[nodiscard]] Task* createParseTask(Resource&) override; + [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override; RESOURCE_HELPERS(DataPack) }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 32b940c5b..f0cdfaff6 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include "MTPixmapCache.h" #include "MetadataHandler.h" @@ -49,6 +50,34 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" +namespace { + +int compareVersionLists(const QStringList& leftVersions, const QStringList& rightVersions) +{ + const qsizetype commonSize = std::min(leftVersions.size(), rightVersions.size()); + + for (qsizetype i = 0; i < commonSize; i++) { + const auto leftVersion = Version(leftVersions.at(i).trimmed()); + const auto rightVersion = Version(rightVersions.at(i).trimmed()); + + if (leftVersion > rightVersion) + return 1; + + if (leftVersion < rightVersion) + return -1; + } + + if (leftVersions.size() > rightVersions.size()) + return 1; + + if (leftVersions.size() < rightVersions.size()) + return -1; + + return 0; +} + +} // namespace + Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); @@ -61,18 +90,18 @@ void Mod::setDetails(const ModDetails& details) int Mod::compare(const Resource& other, SortType type) const { - auto cast_other = dynamic_cast(&other); + auto cast_other = dynamic_cast(&other); if (!cast_other) return Resource::compare(other, type); switch (type) { default: - case SortType::ENABLED: - case SortType::NAME: - case SortType::DATE: - case SortType::SIZE: + case SortType::Enabled: + case SortType::Name: + case SortType::Date: + case SortType::Size: return Resource::compare(other, type); - case SortType::VERSION: { + case SortType::Version: { auto this_ver = Version(version()); auto other_ver = Version(cast_other->version()); if (this_ver > other_ver) @@ -81,30 +110,44 @@ int Mod::compare(const Resource& other, SortType type) const return -1; break; } - case SortType::SIDE: { + case SortType::Side: { auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } - case SortType::MC_VERSIONS: { - auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); + case SortType::McVersions: { + auto compare_result = compareVersionLists(mcVersions(), cast_other->mcVersions()); if (compare_result != 0) return compare_result; break; } - case SortType::LOADERS: { + case SortType::Loaders: { auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } - case SortType::RELEASE_TYPE: { + case SortType::ReleaseType: { auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } + case SortType::RequiredBy: { + if (requiredByCount() > cast_other->requiredByCount()) + return 1; + if (requiredByCount() < cast_other->requiredByCount()) + return -1; + break; + } + case SortType::Requires: { + if (requiresCount() > cast_other->requiresCount()) + return 1; + if (requiresCount() < cast_other->requiresCount()) + return -1; + break; + } } return 0; } @@ -183,14 +226,19 @@ auto Mod::side() const -> QString return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); } -auto Mod::mcVersions() const -> QString +auto Mod::mcVersions() const -> QStringList { if (metadata()) - return metadata()->mcVersions.join(", "); + return metadata()->mcVersions; return {}; } +auto Mod::mcVersionsString() const -> QString +{ + return mcVersions().join(", "); +} + auto Mod::releaseType() const -> QString { if (metadata()) @@ -283,3 +331,25 @@ bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } + +QStringList Mod::dependencies() const +{ + return details().dependencies; +} + +int Mod::requiredByCount() const +{ + return m_requiredByCount; +} +int Mod::requiresCount() const +{ + return m_requiresCount; +} +void Mod::setRequiredByCount(int value) +{ + m_requiredByCount = value; +} +void Mod::setRequiresCount(int value) +{ + m_requiresCount = value; +} diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index c548f5350..27768ae2c 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -68,8 +68,16 @@ class Mod : public Resource { auto issueTracker() const -> QString; auto side() const -> QString; auto loaders() const -> QString; - auto mcVersions() const -> QString; + auto mcVersions() const -> QStringList; + auto mcVersionsString() const -> QString; auto releaseType() const -> QString; + QStringList dependencies() const; + + int requiredByCount() const; + int requiresCount() const; + + void setRequiredByCount(int value); + void setRequiresCount(int value); /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } @@ -102,4 +110,7 @@ class Mod : public Resource { bool wasEverUsed = false; bool wasReadAttempt = false; } mutable m_packImageCacheKey; + + int m_requiredByCount = 0; + int m_requiresCount = 0; }; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 9b81f561f..02cf42a38 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -138,6 +138,8 @@ struct ModDetails { /* Path of mod logo */ QString icon_file = {}; + QStringList dependencies = {}; + ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ @@ -152,6 +154,7 @@ struct ModDetails { , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) + , dependencies(other.dependencies) {} ModDetails& operator=(const ModDetails& other) = default; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 45ec76f19..a03410fd3 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -38,7 +38,9 @@ #include "ModFolderModel.h" #include +#include #include +#include #include #include #include @@ -48,38 +50,49 @@ #include #include #include +#include +#include +#include "minecraft/mod/Resource.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "modplatform/ModIndex.h" +#include "ui/dialogs/CustomMessageBox.h" -ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) - : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", - "Minecraft Versions", "Release Type" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), - tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, - SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, - SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; + m_columnNames = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", + "Minecraft Versions", "Release Type", "Requires", "Required By", "File Name" }); + m_columnNamesTranslated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), + tr("Loaders"), tr("Minecraft Versions"), tr("Release Type"), tr("Requires"), tr("Required By"), tr("File Name") }); + m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::Version, SortType::Date, + SortType::Provider, SortType::Size, SortType::Side, SortType::Loaders, SortType::McVersions, + SortType::ReleaseType, SortType::Requires, SortType::RequiredBy, SortType::Filename }; + m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true, true, true, true }; + + connect(this, &ModFolderModel::parseFinished, this, &ModFolderModel::onParseFinished); } QVariant ModFolderModel::data(const QModelIndex& index, int role) const { - if (!validateIndex(index)) + if (!validateIndex(index)) { return {}; + } int row = index.row(); int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { - case NameColumn: - return m_resources[row]->name(); case VersionColumn: { switch (at(row).type()) { case ResourceType::FOLDER: @@ -87,14 +100,8 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case ResourceType::SINGLEFILE: return tr("File"); default: - break; + return at(row).version(); } - return at(row).version(); - } - case DateColumn: - return at(row).dateTimeChanged(); - case ProviderColumn: { - return at(row).provider(); } case SideColumn: { return at(row).side(); @@ -103,51 +110,66 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const return at(row).loaders(); } case McVersionsColumn: { - return at(row).mcVersions(); + return at(row).mcVersionsString(); } case ReleaseTypeColumn: { return at(row).releaseType(); } - case SizeColumn: - return at(row).sizeStr(); + case RequiredByColumn: { + return at(row).requiredByCount(); + } + case RequiresColumn: { + return at(row).requiresCount(); + } default: - return QVariant(); + break; } - - case Qt::ToolTipRole: - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - return QVariant(); + break; default: - return QVariant(); + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + case FileNameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn); + break; + default: + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const @@ -166,6 +188,9 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case McVersionsColumn: case ReleaseTypeColumn: case SizeColumn: + case RequiredByColumn: + case RequiresColumn: + case FileNameColumn: return columnNames().at(section); default: return QVariant(); @@ -193,23 +218,28 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio return tr("The release type."); case SizeColumn: return tr("The size of the mod."); + case RequiredByColumn: + return tr("For each mod, the number of other mods which depend on it."); + case RequiresColumn: + return tr("For each mod, the number of other mods it depends on."); + case FileNameColumn: + return tr("The file name of the mod."); default: return QVariant(); } default: return QVariant(); } - return QVariant(); } int ModFolderModel::columnCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : NUM_COLUMNS; + return parent.isValid() ? 0 : NumColumns; } Task* ModFolderModel::createParseTask(Resource& resource) { - return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); + return new LocalModParseTask(m_nextResolutionTicket, resource.type(), resource.fileinfo()); } bool ModFolderModel::isValid() @@ -217,24 +247,288 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } -void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) +void ModFolderModel::onParseSucceeded(int ticket, const QString& resourceId) { - auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd()) + auto iter = m_activeParseTasks.constFind(ticket); + if (iter == m_activeParseTasks.constEnd()) { return; + } - int row = m_resources_index[mod_id]; + int row = m_resourcesIndex[resourceId]; - auto parse_task = *iter; - auto cast_task = static_cast(parse_task.get()); + const auto& parseTask = *iter; + auto* castTask = static_cast(parseTask.get()); - Q_ASSERT(cast_task->token() == ticket); + Q_ASSERT(castTask->token() == ticket); - auto resource = find(mod_id); + auto resource = find(resourceId); - auto result = cast_task->result(); - if (result && resource) - static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); - - emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + auto result = castTask->result(); + if (result && resource) { + auto* mod = static_cast(resource.get()); + mod->finishResolvingWithDetails(std::move(result->details)); + } + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); +} + +namespace { +Mod* findById(QSet mods, const QString& resourceId) +{ + auto found = std::ranges::find_if(mods, [resourceId](Mod* m) { return m->mod_id() == resourceId; }); + return found != mods.end() ? *found : nullptr; +} +} // namespace + +void ModFolderModel::onParseFinished() +{ + if (hasPendingParseTasks()) { + return; + } + auto modsList = allMods(); + auto mods = QSet(modsList.begin(), modsList.end()); + + m_requires.clear(); + m_requiredBy.clear(); + + auto findByProjectID = [mods](const QVariant& modId, ModPlatform::ResourceProvider provider) -> Mod* { + auto found = std::ranges::find_if(mods, [modId, provider](Mod* m) { + return m->metadata() && m->metadata()->provider == provider && m->metadata()->project_id == modId; + }); + return found != mods.end() ? *found : nullptr; + }; + for (auto* mod : mods) { + auto id = mod->mod_id(); + for (const auto& dep : mod->dependencies()) { + auto* d = findById(mods, dep); + if (d) { + m_requires[id] << d; + m_requiredBy[d->mod_id()] << mod; + } + } + if (mod->metadata()) { + for (const auto& dep : mod->metadata()->dependencies) { + if (dep.type == ModPlatform::DependencyType::REQUIRED) { + auto* d = findByProjectID(dep.addonId, mod->metadata()->provider); + if (d) { + m_requires[id] << d; + m_requiredBy[d->mod_id()] << mod; + } + } + } + } + } + for (auto* mod : mods) { + auto id = mod->mod_id(); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resourcesIndex[mod->internalId()]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); + } + } +} + +namespace { + +QSet collectMods(const QSet& mods, QHash> relation, std::set& seen, bool shouldBeEnabled) +{ + QSet affectedList = {}; + QSet needToCheck = {}; + for (auto* mod : mods) { + auto id = mod->mod_id(); + if (!seen.contains(id)) { + seen.insert(id); + for (auto* affected : relation[id]) { + auto affectedId = affected->mod_id(); + + if (findById(mods, affectedId) == nullptr && !seen.contains(affectedId)) { + if (shouldBeEnabled != affected->enabled()) { + affectedList << affected; + } + needToCheck << affected; + } + } + } + } + // collect the affected mods until all of them are included in the list + if (!needToCheck.isEmpty()) { + affectedList += collectMods(needToCheck, relation, seen, shouldBeEnabled); + } + return affectedList; +} +} // namespace + +QModelIndexList ModFolderModel::getAffectedMods(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) { + return {}; + } + + QModelIndexList affectedList = {}; + auto affectedModsList = selectedMods(indexes); + auto affectedMods = QSet(affectedModsList.begin(), affectedModsList.end()); + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + affectedMods = collectMods(affectedMods, m_requires, seen, true); + break; + } + case EnableAction::DISABLE: { + affectedMods = collectMods(affectedMods, m_requiredBy, seen, false); + break; + } + case EnableAction::TOGGLE: { + return {}; // this function should not be called with TOGGLE + } + } + for (auto* affected : affectedMods) { + auto affectedId = affected->mod_id(); + auto row = m_resourcesIndex[affected->internalId()]; + affectedList << index(row, 0); + } + return affectedList; +} + +bool ModFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + if (indexes.isEmpty()) { + return {}; + } + + auto indexedModsList = selectedMods(indexes); + auto indexedMods = QSet(indexedModsList.begin(), indexedModsList.end()); + + QSet toEnable = {}; + QSet toDisable = {}; + std::set seen; + + switch (action) { + case EnableAction::ENABLE: { + toEnable = indexedMods; + break; + } + case EnableAction::DISABLE: { + toDisable = indexedMods; + break; + } + case EnableAction::TOGGLE: { + for (auto* mod : indexedMods) { + if (mod->enabled()) { + toDisable << mod; + } else { + toEnable << mod; + } + } + break; + } + } + + auto requiredToEnable = collectMods(toEnable, m_requires, seen, true); + auto requiredToDisable = collectMods(toDisable, m_requiredBy, seen, false); + + toDisable.removeIf([toEnable](Mod* m) { return toEnable.contains(m); }); + auto toList = [this](const QSet& mods) { + QModelIndexList list; + for (auto* mod : mods) { + auto row = m_resourcesIndex[mod->internalId()]; + list << index(row, 0); + } + return list; + }; + + if (requiredToEnable.size() > 0 || requiredToDisable.size() > 0) { + QString title; + QString message; + QString noButton; + QString yesButton; + if (requiredToEnable.size() > 0 && requiredToDisable.size() > 0) { + title = tr("Confirm toggle"); + message = tr("Toggling these mod(s) will cause changes to other mods.\n") + + tr("%n mod(s) will be enabled\n", "", requiredToEnable.size()) + + tr("%n mod(s) will be disabled\n", "", requiredToDisable.size()) + + tr("Do you want to automatically apply these related changes?\nIgnoring them may break the game."); + noButton = tr("Only Toggle Selected"); + yesButton = tr("Toggle Required Mods"); + } else if (requiredToEnable.size() > 0) { + title = tr("Confirm enable"); + message = tr("The enabled mod(s) require %n mod(s).\n", "", requiredToEnable.size()) + + tr("Would you like to enable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Enable Selected"); + yesButton = tr("Enable Required"); + } else { + title = tr("Confirm disable"); + message = tr("The disabled mod(s) are required by %n mod(s).\n", "", requiredToDisable.size()) + + tr("Would you like to disable them as well?\nIgnoring them may break the game."); + noButton = tr("Only Disable Selected"); + yesButton = tr("Disable Required"); + } + + auto* box = CustomMessageBox::selectable(nullptr, title, message, QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); + box->button(QMessageBox::No)->setText(noButton); + box->button(QMessageBox::Yes)->setText(yesButton); + auto response = box->exec(); + + if (response == QMessageBox::Yes) { + toEnable |= requiredToEnable; + toDisable |= requiredToDisable; + } else if (response == QMessageBox::Cancel) { + return false; + } + } + + auto disableStatus = ResourceFolderModel::setResourceEnabled(toList(toDisable), EnableAction::DISABLE); + auto enableStatus = ResourceFolderModel::setResourceEnabled(toList(toEnable), EnableAction::ENABLE); + return disableStatus && enableStatus; +} + +namespace { +QStringList reqToList(const QSet& l) +{ + QStringList req; + for (auto* m : l) { + req << m->name(); + } + return req; +} +} // namespace + +QStringList ModFolderModel::requiresList(const QString& id) +{ + return reqToList(m_requires[id]); +} + +QStringList ModFolderModel::requiredByList(const QString& id) +{ + return reqToList(m_requiredBy[id]); +} + +bool ModFolderModel::deleteResources(const QModelIndexList& indexes) +{ + auto deleteInvalid = [](QSet& mods) { + for (auto it = mods.begin(); it != mods.end();) { + auto* mod = *it; + // the QFileInfo::exists is used instead of mod->fileinfo().exists + // because the later somehow caches that the file exists + if (!mod || !QFileInfo::exists(mod->fileinfo().absoluteFilePath())) { + it = mods.erase(it); + } else { + ++it; + } + } + }; + auto rsp = ResourceFolderModel::deleteResources(indexes); + for (auto* mod : allMods()) { + auto id = mod->mod_id(); + deleteInvalid(m_requiredBy[id]); + deleteInvalid(m_requires[id]); + if (mod->requiredByCount() != m_requiredBy[id].count() || mod->requiresCount() != m_requires[id].count()) { + mod->setRequiredByCount(m_requiredBy[id].count()); + mod->setRequiresCount(m_requires[id].count()); + int row = m_resourcesIndex[mod->internalId()]; + emit dataChanged(index(row, RequiresColumn), index(row, RequiredByColumn)); + } + } + return rsp; } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 42868dc91..a5c6ba483 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -39,13 +39,14 @@ #include #include -#include +#include #include #include #include #include "Mod.h" #include "ResourceFolderModel.h" +#include "minecraft/mod/Resource.h" class BaseInstance; class QFileSystemWatcher; @@ -57,7 +58,7 @@ class QFileSystemWatcher; class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { + enum Columns : std::uint8_t { ActiveColumn = 0, ImageColumn, NameColumn, @@ -69,11 +70,14 @@ class ModFolderModel : public ResourceFolderModel { LoadersColumn, McVersionsColumn, ReleaseTypeColumn, - NUM_COLUMNS + RequiresColumn, + RequiredByColumn, + FileNameColumn, + NumColumns }; - ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + ModFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr); - virtual QString id() const override { return "mods"; } + QString id() const override { return "mods"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -81,12 +85,26 @@ class ModFolderModel : public ResourceFolderModel { int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } - [[nodiscard]] Task* createParseTask(Resource&) override; + [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override; bool isValid(); + bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action) override; + bool deleteResources(const QModelIndexList& indexes) override; + + QModelIndexList getAffectedMods(const QModelIndexList& indexes, EnableAction action); + RESOURCE_HELPERS(Mod) + public: + QStringList requiresList(const QString& id); + QStringList requiredByList(const QString& id); + private slots: - void onParseSucceeded(int ticket, QString resource_id) override; + void onParseSucceeded(int ticket, const QString& resourceId) override; + void onParseFinished(); + + private: + QHash> m_requiredBy; + QHash> m_requires; }; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 8f67d5d94..06d9a0af7 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -4,69 +4,75 @@ #include #include #include +#include #include "FileSystem.h" #include "StringUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" -Resource::Resource(QObject* parent) : QObject(parent) {} +Resource::Resource(QObject* parent) : QObject(parent), m_size_info(0) {} -Resource::Resource(QFileInfo file_info) : QObject() +Resource::Resource(QFileInfo fileInfo) : m_size_info(0) { - setFile(file_info); + setFile(fileInfo); } -void Resource::setFile(QFileInfo file_info) +void Resource::setFile(QFileInfo fileInfo) { - m_file_info = file_info; + m_file_info = std::move(fileInfo); parseFile(); } -static std::tuple calculateFileSize(const QFileInfo& file) +namespace { +std::tuple calculateFileSize(const QFileInfo& file) { if (file.isDir()) { auto dir = QDir(file.absoluteFilePath()); dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); auto count = dir.count(); auto str = QObject::tr("item"); - if (count != 1) + if (count != 1) { str = QObject::tr("items"); + } return { QString("%1 %2").arg(QString::number(count), str), count }; } return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; } +} // namespace void Resource::parseFile() { - QString file_name{ m_file_info.fileName() }; + QString fileName{ m_file_info.fileName() }; m_type = ResourceType::UNKNOWN; - m_internal_id = file_name; + m_internal_id = fileName; std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); if (m_file_info.isDir()) { m_type = ResourceType::FOLDER; - m_name = file_name; + m_name = fileName; } else if (m_file_info.isFile()) { - if (file_name.endsWith(".disabled")) { - file_name.chop(9); + if (fileName.endsWith(".disabled")) { + fileName.chop(9); m_enabled = false; } - if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { + if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) { m_type = ResourceType::ZIPFILE; - file_name.chop(4); - } else if (file_name.endsWith(".nilmod")) { + fileName.chop(4); + } else if (fileName.endsWith(".nilmod")) { m_type = ResourceType::ZIPFILE; - file_name.chop(7); - } else if (file_name.endsWith(".litemod")) { + fileName.chop(7); + } else if (fileName.endsWith(".litemod")) { m_type = ResourceType::LITEMOD; - file_name.chop(8); + fileName.chop(8); } else { m_type = ResourceType::SINGLEFILE; } - m_name = file_name; + m_name = fileName; } m_changed_date_time = m_file_info.lastModified(); @@ -74,87 +80,140 @@ void Resource::parseFile() auto Resource::name() const -> QString { - if (metadata()) + if (metadata()) { return metadata()->name; + } return m_name; } -static void removeThePrefix(QString& string) +namespace { +void removeThePrefix(QString& string) { static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); string.remove(s_regex); string = string.trimmed(); } +} // namespace auto Resource::provider() const -> QString { - if (metadata()) + if (metadata()) { return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); + } return tr("Unknown"); } auto Resource::homepage() const -> QString { - if (metadata()) + if (metadata()) { return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + } return {}; } void Resource::setMetadata(std::shared_ptr&& metadata) { - if (status() == ResourceStatus::NO_METADATA) - setStatus(ResourceStatus::INSTALLED); + if (status() == ResourceStatus::NoMetadata) { + setStatus(ResourceStatus::Installed); + } m_metadata = metadata; } +QStringList Resource::issues() const +{ + QStringList result; + result.reserve(m_issues.length()); + + for (const char* issue : m_issues) { + result.append(tr(issue)); + } + + return result; +} + +void Resource::updateIssues(const BaseInstance* inst) +{ + m_issues.clear(); + + if (m_metadata == nullptr) { + return; + } + + const auto* mcInst = dynamic_cast(inst); + if (mcInst == nullptr) { + return; + } + + auto* profile = mcInst->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + + if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { + // delay translation until issues() is called + m_issues.append(QT_TR_NOOP("Not marked as compatible with the instance's game version.")); + } +} + int Resource::compare(const Resource& other, SortType type) const { switch (type) { default: - case SortType::ENABLED: - if (enabled() && !other.enabled()) + case SortType::Enabled: + if (enabled() && !other.enabled()) { return 1; - if (!enabled() && other.enabled()) + } + if (!enabled() && other.enabled()) { return -1; + } break; - case SortType::NAME: { - QString this_name{ name() }; - QString other_name{ other.name() }; + case SortType::Name: { + QString thisName{ name() }; + QString otherName{ other.name() }; // TODO do we need this? it could result in 0 being returned - removeThePrefix(this_name); - removeThePrefix(other_name); + removeThePrefix(thisName); + removeThePrefix(otherName); - return QString::compare(this_name, other_name, Qt::CaseInsensitive); + return QString::compare(thisName, otherName, Qt::CaseInsensitive); } - case SortType::DATE: - if (dateTimeChanged() > other.dateTimeChanged()) + case SortType::Date: + if (dateTimeChanged() > other.dateTimeChanged()) { return 1; - if (dateTimeChanged() < other.dateTimeChanged()) + } + if (dateTimeChanged() < other.dateTimeChanged()) { return -1; + } break; - case SortType::SIZE: { + case SortType::Filename: + return fileinfo().fileName().localeAwareCompare(other.fileinfo().fileName()); + + case SortType::Size: { if (this->type() != other.type()) { - if (this->type() == ResourceType::FOLDER) + if (this->type() == ResourceType::FOLDER) { return -1; - if (other.type() == ResourceType::FOLDER) + } + if (other.type() == ResourceType::FOLDER) { return 1; + } } - if (sizeInfo() > other.sizeInfo()) + if (sizeInfo() > other.sizeInfo()) { return 1; - if (sizeInfo() < other.sizeInfo()) + } + if (sizeInfo() < other.sizeInfo()) { return -1; + } break; } - case SortType::PROVIDER: { - auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); - if (compare_result != 0) - return compare_result; + + case SortType::Provider: { + auto compareResult = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); + if (compareResult != 0) { + return compareResult; + } break; } } @@ -164,13 +223,20 @@ int Resource::compare(const Resource& other, SortType type) const bool Resource::applyFilter(QRegularExpression filter) const { - return filter.match(name()).hasMatch(); + if (filter.match(name()).hasMatch()) { + return true; + } + if (filter.match(fileinfo().fileName()).hasMatch()) { + return true; + } + return false; } bool Resource::enable(EnableAction action) { - if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) { return false; + } QString path = m_file_info.absoluteFilePath(); QFile file(path); @@ -189,14 +255,16 @@ bool Resource::enable(EnableAction action) break; } - if (m_enabled == enable) + if (m_enabled == enable) { return false; + } if (enable) { // m_enabled is false, but there's no '.disabled' suffix. // TODO: Report error? - if (!path.endsWith(".disabled")) + if (!path.endsWith(".disabled")) { return false; + } path.chop(9); } else { path += ".disabled"; @@ -204,8 +272,9 @@ bool Resource::enable(EnableAction action) path = FS::getUniqueResourceName(path); } } - if (!file.rename(path)) + if (!file.rename(path)) { return false; + } setFile(QFileInfo(path)); @@ -213,33 +282,34 @@ bool Resource::enable(EnableAction action) return true; } -auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool +auto Resource::destroy(const QDir& indexDir, bool preserveMetadata, bool attemptTrash) -> bool { m_type = ResourceType::UNKNOWN; - if (!preserve_metadata) { + if (!preserveMetadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); - destroyMetadata(index_dir); + destroyMetadata(indexDir); } - return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); + return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); } -auto Resource::destroyMetadata(const QDir& index_dir) -> void +auto Resource::destroyMetadata(const QDir& indexDir) -> void { if (metadata()) { - Metadata::remove(index_dir, metadata()->slug); + Metadata::remove(indexDir, metadata()->slug); } else { auto n = name(); - Metadata::remove(index_dir, n); + Metadata::remove(indexDir, n); } m_metadata = nullptr; } bool Resource::isSymLinkUnder(const QString& instPath) const { - if (isSymLink()) + if (isSymLink()) { return true; + } auto instDir = QDir(instPath); @@ -257,7 +327,51 @@ bool Resource::isMoreThanOneHardLink() const auto Resource::getOriginalFileName() const -> QString { auto fileName = m_file_info.fileName(); - if (!m_enabled) + if (!m_enabled) { fileName.chop(9); + } return fileName; } + +QDebug operator<<(QDebug debug, ResourceType type) +{ + switch (type) { + case ResourceType::ZIPFILE: + debug << "ZIPFILE"; + break; + case ResourceType::SINGLEFILE: + debug << "SINGLEFILE"; + break; + case ResourceType::FOLDER: + debug << "FOLDER"; + break; + case ResourceType::LITEMOD: + debug << "LITEMOD"; + break; + case ResourceType::UNKNOWN: + default: + debug << "UNKNOWN"; + break; + }; + return debug; +} + +QDebug operator<<(QDebug debug, ResourceStatus status) +{ + switch (status) { + case ResourceStatus::Installed: + debug << "INSTALLED"; + break; + case ResourceStatus::NotInstalled: + debug << "NOT_INSTALLED"; + break; + case ResourceStatus::NoMetadata: + debug << "NO_METADATA"; + break; + case ResourceStatus::Unknown: + default: + debug << "UNKNOWN"; + break; + }; + return debug; +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 87bfd4345..694d74ec0 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -43,7 +43,9 @@ #include "MetadataHandler.h" #include "QObjectPtr.h" -enum class ResourceType { +class BaseInstance; + +enum class ResourceType : std::uint8_t { UNKNOWN, //!< Indicates an unspecified resource type. ZIPFILE, //!< The resource is a zip file containing the resource's class files. SINGLEFILE, //!< The resource is a single file (not a zip file). @@ -51,16 +53,35 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; -enum class ResourceStatus { - INSTALLED, // Both JAR and Metadata are present - NOT_INSTALLED, // Only the Metadata is present - NO_METADATA, // Only the JAR is present - UNKNOWN, // Default status +QDebug operator<<(QDebug debug, ResourceType type); + +enum class ResourceStatus : std::uint8_t { + Installed, // Both JAR and Metadata are present + NotInstalled, // Only the Metadata is present + NoMetadata, // Only the JAR is present + Unknown, // Default status }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; +QDebug operator<<(QDebug debug, ResourceStatus status); -enum class EnableAction { ENABLE, DISABLE, TOGGLE }; +enum class SortType : std::uint8_t { + Name, + Date, + Version, + Enabled, + PackFormat, + Provider, + Size, + Side, + McVersions, + Loaders, + ReleaseType, + Requires, + RequiredBy, + Filename, +}; + +enum class EnableAction : std::uint8_t { ENABLE, DISABLE, TOGGLE }; /** General class for managed resources. It mirrors a file in disk, with some more info * for display and house-keeping purposes. @@ -72,20 +93,19 @@ class Resource : public QObject { Q_DISABLE_COPY(Resource) public: using Ptr = shared_qobject_ptr; - using WeakPtr = QPointer; Resource(QObject* parent = nullptr); - Resource(QFileInfo file_info); - Resource(QString file_path) : Resource(QFileInfo(file_path)) {} + Resource(QFileInfo fileInfo); + Resource(const QString& filePath) : Resource(QFileInfo(filePath)) {} ~Resource() override = default; - void setFile(QFileInfo file_info); + void setFile(QFileInfo fileInfo); void parseFile(); auto fileinfo() const -> QFileInfo { return m_file_info; } auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } - auto internal_id() const -> QString { return m_internal_id; } + auto internalId() const -> QString { return m_internal_id; } auto type() const -> ResourceType { return m_type; } bool enabled() const { return m_enabled; } auto getOriginalFileName() const -> QString; @@ -105,12 +125,20 @@ class Resource : public QObject { void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } + /** + * Returns compatibility issues with the resource and the instance. + * This is initially empty, and may be updated when calling updateIssues. + */ + QStringList issues() const; + void updateIssues(const BaseInstance* inst); + bool hasIssues() const { return !m_issues.empty(); } + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' */ - virtual int compare(Resource const& other, SortType type = SortType::NAME) const; + virtual int compare(const Resource& other, SortType type = SortType::Name) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). @@ -135,9 +163,9 @@ class Resource : public QObject { } // Delete all files of this resource. - auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + auto destroy(const QDir& indexDir, bool preserveMetadata = false, bool attemptTrash = true) -> bool; // Delete the metadata only. - auto destroyMetadata(const QDir& index_dir) -> void; + auto destroyMetadata(const QDir& indexDir) -> void; auto isSymLink() const -> bool { return m_file_info.isSymLink(); } @@ -152,9 +180,6 @@ class Resource : public QObject { bool isMoreThanOneHardLink() const; - auto mod_id() const -> QString { return m_mod_id; } - void setModId(const QString& modId) { m_mod_id = modId; } - protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; @@ -165,19 +190,20 @@ class Resource : public QObject { QString m_internal_id; /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ QString m_name; - QString m_mod_id; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; /* Installation status of the resource. */ - ResourceStatus m_status = ResourceStatus::UNKNOWN; + ResourceStatus m_status = ResourceStatus::Unknown; std::shared_ptr m_metadata = nullptr; /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; + QList m_issues; + /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; bool m_is_resolved = false; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 1d5c7ca79..ddc132089 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "Application.h" @@ -27,10 +28,10 @@ #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" -ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) - : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) +ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent) + : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_isIndexed(isIndexed) { - if (create_dir) { + if (createDir) { FS::ensureFolderPathExists(m_dir.absolutePath()); } @@ -49,70 +50,75 @@ ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance ResourceFolderModel::~ResourceFolderModel() { - while (!QThreadPool::globalInstance()->waitForDone(100)) + while (!QThreadPool::globalInstance()->waitForDone(100)) { QCoreApplication::processEvents(); + } } bool ResourceFolderModel::startWatching(const QStringList& paths) { // Remove orphaned metadata next time - m_first_folder_load = true; + m_firstFolderLoad = true; - if (m_is_watching) + if (m_isWatching) { return false; + } - auto couldnt_be_watched = m_watcher.addPaths(paths); - for (auto path : paths) { - if (couldnt_be_watched.contains(path)) + auto couldntBeWatched = m_watcher.addPaths(paths); + for (const auto& path : paths) { + if (couldntBeWatched.contains(path)) { qDebug() << "Failed to start watching" << path; - else + } else { qDebug() << "Started watching" << path; + } } update(); - m_is_watching = !m_is_watching; - return m_is_watching; + m_isWatching = !m_isWatching; + return m_isWatching; } bool ResourceFolderModel::stopWatching(const QStringList& paths) { - if (!m_is_watching) + if (!m_isWatching) { return false; - - auto couldnt_be_stopped = m_watcher.removePaths(paths); - for (auto path : paths) { - if (couldnt_be_stopped.contains(path)) - qDebug() << "Failed to stop watching" << path; - else - qDebug() << "Stopped watching" << path; } - m_is_watching = !m_is_watching; - return !m_is_watching; + auto couldntBeStopped = m_watcher.removePaths(paths); + for (const auto& path : paths) { + if (couldntBeStopped.contains(path)) { + qDebug() << "Failed to stop watching" << path; + } else { + qDebug() << "Stopped watching" << path; + } + } + + m_isWatching = !m_isWatching; + return !m_isWatching; } -bool ResourceFolderModel::installResource(QString original_path) +bool ResourceFolderModel::installResource(QString originalPath) { // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - original_path = FS::NormalizePath(original_path); - QFileInfo file_info(original_path); + originalPath = FS::NormalizePath(originalPath); + QFileInfo fileInfo(originalPath); - if (!file_info.exists() || !file_info.isReadable()) { - qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; + if (!fileInfo.exists() || !fileInfo.isReadable()) { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath; return false; } - qDebug() << "Installing:" << file_info.absoluteFilePath(); + qDebug() << "Installing:" << fileInfo.absoluteFilePath(); - Resource resource(file_info); + Resource resource(fileInfo); if (!resource.valid()) { - qWarning() << original_path << "is not a valid resource. Ignoring it."; + qWarning() << originalPath << "is not a valid resource. Ignoring it."; return false; } - auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); - if (original_path == new_path) { - qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; + auto newPath = FS::NormalizePath(m_dir.filePath(fileInfo.fileName())); + if (originalPath == newPath) { + qWarning() << "Overwriting the mod (" << originalPath << ") with itself makes no sense..."; return false; } @@ -120,45 +126,47 @@ bool ResourceFolderModel::installResource(QString original_path) case ResourceType::SINGLEFILE: case ResourceType::ZIPFILE: case ResourceType::LITEMOD: { - if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { - if (!FS::deletePath(new_path)) { - qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; + if (QFile::exists(newPath) || QFile::exists(newPath + QString(".disabled"))) { + if (!FS::deletePath(newPath)) { + qCritical() << "Cleaning up new location (" << newPath << ") was unsuccessful!"; return false; } - qDebug() << new_path << "has been deleted."; + qDebug() << newPath << "has been deleted."; } - if (!QFile::copy(original_path, new_path)) { - qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; + if (!QFile::copy(originalPath, newPath)) { + qCritical() << "Copy from" << originalPath << "to" << newPath << "has failed."; return false; } - FS::updateTimestamp(new_path); + FS::updateTimestamp(newPath); - QFileInfo new_path_file_info(new_path); - resource.setFile(new_path_file_info); + QFileInfo newPathFileInfo(newPath); + resource.setFile(newPathFileInfo); - if (!m_is_watching) + if (!m_isWatching) { return update(); + } return true; } case ResourceType::FOLDER: { - if (QFile::exists(new_path)) { - qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; + if (QFile::exists(newPath)) { + qDebug() << "Ignoring folder '" << originalPath << "', it would merge with" << newPath; return false; } - if (!FS::copy(original_path, new_path)()) { - qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; + if (!FS::copy(originalPath, newPath)()) { + qWarning() << "Copy of folder from" << originalPath << "to" << newPath << "has (potentially partially) failed."; return false; } - QFileInfo newpathInfo(new_path); + QFileInfo newpathInfo(newPath); resource.setFile(newpathInfo); - if (!m_is_watching) + if (!m_isWatching) { return update(); + } return true; } @@ -168,25 +176,24 @@ bool ResourceFolderModel::installResource(QString original_path) return false; } -void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +void ResourceFolderModel::installResourceWithFlameMetadata(const QString& path, ModPlatform::IndexedVersion& vers) { - auto install = [this, path] { installResource(std::move(path)); }; + auto install = [this, path] { installResource(path); }; if (vers.addonId.isValid()) { ModPlatform::IndexedPack pack{ - vers.addonId, - ModPlatform::ResourceProvider::FLAME, + .addonId = vers.addonId, + .provider = ModPlatform::ResourceProvider::FLAME, }; - auto response = std::make_shared(); - auto job = FlameAPI().getProject(vers.addonId.toString(), response); + auto [job, response] = FlameAPI().getProject(vers.addonId.toString()); connect(job.get(), &Task::failed, this, install); connect(job.get(), &Task::aborted, this, install); connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset - << "reason:" << parse_error.errorString(); + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at" << parseError.offset + << "reason:" << parseError.errorString(); qDebug() << *response; return; } @@ -197,9 +204,9 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat qDebug() << doc; qWarning() << "Error while reading mod info:" << e.cause(); } - LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); - connect(&update_metadata, &Task::finished, this, install); - update_metadata.start(); + LocalResourceUpdateTask updateMetadata(indexDir(), pack, vers); + connect(&updateMetadata, &Task::finished, this, install); + updateMetadata.start(); }); job->start(); @@ -208,7 +215,7 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat } } -bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) +bool ResourceFolderModel::uninstallResource(const QString& fileName, bool preserveMetadata) { for (auto& resource : m_resources) { auto resourceFileInfo = resource->fileinfo(); @@ -217,8 +224,8 @@ bool ResourceFolderModel::uninstallResource(const QString& file_name, bool prese resourceFileName.chop(9); } - if (resourceFileName == file_name) { - auto res = resource->destroy(indexDir(), preserve_metadata, false); + if (resourceFileName == fileName) { + auto res = resource->destroy(indexDir(), preserveMetadata, false); update(); @@ -230,14 +237,16 @@ bool ResourceFolderModel::uninstallResource(const QString& file_name, bool prese bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) { - if (indexes.isEmpty()) + if (indexes.isEmpty()) { return true; + } for (auto i : indexes) { - if (i.column() != 0) + if (i.column() != 0) { continue; + } - auto& resource = m_resources.at(i.row()); + const auto& resource = m_resources.at(i.row()); resource->destroy(indexDir()); } @@ -248,14 +257,16 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) { - if (indexes.isEmpty()) + if (indexes.isEmpty()) { return; + } for (auto i : indexes) { - if (i.column() != 0) + if (i.column() != 0) { continue; + } - auto& resource = m_resources.at(i.row()); + const auto& resource = m_resources.at(i.row()); resource->destroyMetadata(indexDir()); } @@ -272,33 +283,36 @@ bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, Ena QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return false; + } } - if (indexes.isEmpty()) + if (indexes.isEmpty()) { return true; + } bool succeeded = true; - for (auto const& idx : indexes) { - if (!validateIndex(idx) || idx.column() != 0) + for (const auto& idx : indexes) { + if (!validateIndex(idx) || idx.column() != 0) { continue; + } int row = idx.row(); auto& resource = m_resources[row]; // Preserve the row, but change its ID - auto old_id = resource->internal_id(); + auto oldId = resource->internalId(); if (!resource->enable(action)) { succeeded = false; continue; } - auto new_id = resource->internal_id(); + auto newId = resource->internalId(); - m_resources_index.remove(old_id); - m_resources_index[new_id] = row; + m_resourcesIndex.remove(oldId); + m_resourcesIndex[newId] = row; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -313,24 +327,25 @@ bool ResourceFolderModel::update() QMutexLocker lock(&s_update_task_mutex); // Already updating, so we schedule a future update and return. - if (m_current_update_task) { - m_scheduled_update = true; + if (m_currentUpdateTask) { + m_scheduledUpdate = true; return false; } - m_current_update_task.reset(createUpdateTask()); - if (!m_current_update_task) + m_currentUpdateTask.reset(createUpdateTask()); + if (!m_currentUpdateTask) { return false; + } - connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, + connect(m_currentUpdateTask.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); - connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); + connect(m_currentUpdateTask.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect( - m_current_update_task.get(), &Task::finished, this, + m_currentUpdateTask.get(), &Task::finished, this, [this] { - m_current_update_task.reset(); - if (m_scheduled_update) { - m_scheduled_update = false; + m_currentUpdateTask.reset(); + if (m_scheduledUpdate) { + m_scheduledUpdate = false; update(); } else { emit updateFinished(); @@ -341,16 +356,16 @@ bool ResourceFolderModel::update() Task::Ptr preUpdate{ createPreUpdateTask() }; if (preUpdate != nullptr) { - auto task = new SequentialTask("ResourceFolderModel::update"); + auto* task = new SequentialTask("ResourceFolderModel::update"); task->addTask(preUpdate); - task->addTask(m_current_update_task); + task->addTask(m_currentUpdateTask); connect(task, &Task::finished, [task] { task->deleteLater(); }); QThreadPool::globalInstance()->start(task); } else { - QThreadPool::globalInstance()->start(m_current_update_task.get()); + QThreadPool::globalInstance()->start(m_currentUpdateTask.get()); } return true; @@ -363,24 +378,25 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) } Task::Ptr task{ createParseTask(*res) }; - if (!task) + if (!task) { return; + } - int ticket = m_next_resolution_ticket.fetch_add(1); + int ticket = m_nextResolutionTicket.fetch_add(1); res->setResolving(true, ticket); - m_active_parse_tasks.insert(ticket, task); + m_activeParseTasks.insert(ticket, task); connect( - task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internalId()); }, Qt::ConnectionType::QueuedConnection); connect( - task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, + task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internalId()); }, Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::finished, this, [this, ticket] { - m_active_parse_tasks.remove(ticket); + m_activeParseTasks.remove(ticket); emit parseFinished(); }, Qt::ConnectionType::QueuedConnection); @@ -395,44 +411,45 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) void ResourceFolderModel::onUpdateSucceeded() { - auto update_results = static_cast(m_current_update_task.get())->result(); + auto updateResults = static_cast(m_currentUpdateTask.get())->result(); - auto& new_resources = update_results->resources; + auto& newResources = updateResults->resources; - auto current_list = m_resources_index.keys(); - QSet current_set(current_list.begin(), current_list.end()); + auto currentList = m_resourcesIndex.keys(); + QSet currentSet(currentList.begin(), currentList.end()); - auto new_list = new_resources.keys(); - QSet new_set(new_list.begin(), new_list.end()); + auto newList = newResources.keys(); + QSet newSet(newList.begin(), newList.end()); - applyUpdates(current_set, new_set, new_resources); + applyUpdates(currentSet, newSet, newResources); } -void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) +void ResourceFolderModel::onParseSucceeded(int ticket, const QString& resourceId) { - auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + auto iter = m_activeParseTasks.constFind(ticket); + if (iter == m_activeParseTasks.constEnd() || !m_resourcesIndex.contains(resourceId)) { return; + } - int row = m_resources_index[resource_id]; + int row = m_resourcesIndex[resourceId]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } Task* ResourceFolderModel::createUpdateTask() { - auto index_dir = indexDir(); - auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, - [this](const QFileInfo& file) { return createResource(file); }); - m_first_folder_load = false; + auto indexDir2 = indexDir(); + auto* task = new ResourceFolderLoadTask(dir(), indexDir2, m_isIndexed, m_firstFolderLoad, + [this](const QFileInfo& file) { return createResource(file); }); + m_firstFolderLoad = false; return task; } bool ResourceFolderModel::hasPendingParseTasks() const { - return !m_active_parse_tasks.isEmpty(); + return !m_activeParseTasks.isEmpty(); } -void ResourceFolderModel::directoryChanged(QString path) +void ResourceFolderModel::directoryChanged(const QString& /*path*/) { update(); } @@ -447,8 +464,9 @@ Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); auto flags = defaultFlags | Qt::ItemIsDropEnabled; - if (index.isValid()) + if (index.isValid()) { flags |= Qt::ItemIsUserCheckable; + } return flags; } @@ -459,21 +477,25 @@ QStringList ResourceFolderModel::mimeTypes() const return types; } -bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +bool ResourceFolderModel::dropMimeData(const QMimeData* data, + Qt::DropAction action, + int /*row*/, + int /*column*/, + const QModelIndex& /*parent*/) { if (action == Qt::IgnoreAction) { return true; } // check if the action is supported - if (!data || !(action & supportedDropActions())) { + if ((data == nullptr) || !(action & supportedDropActions())) { return false; } // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); - for (auto url : urls) { + for (const auto& url : urls) { // only local files may be dropped... if (!url.isLocalFile()) { continue; @@ -489,25 +511,36 @@ bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction act bool ResourceFolderModel::validateIndex(const QModelIndex& index) const { - if (!index.isValid()) + if (!index.isValid()) { return false; + } int row = index.row(); - if (row < 0 || row >= m_resources.size()) - return false; + return row >= 0 && row < m_resources.size(); +} - return true; +// HACK: all subclasses need to call this to have the whole row painted +// and they only delegate to the superclass for compatible columns +QBrush ResourceFolderModel::rowBackground(int row) const +{ + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) { + return { QColor(255, 0, 0, 40) }; + } + return {}; } QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const { - if (!validateIndex(index)) + if (!validateIndex(index)) { return {}; + } int row = index.row(); int column = index.column(); switch (role) { + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DisplayRole: switch (column) { case NameColumn: @@ -518,34 +551,52 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); + case FileNameColumn: + return m_resources[row]->fileinfo().fileName(); default: return {}; } - case Qt::ToolTipRole: + case Qt::ToolTipRole: { + QString tooltip = m_resources[row]->internalId(); + if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; + if (APPLICATION->settings()->get("ShowModIncompat").toBool()) { + for (const QString& issue : at(row).issues()) { + tooltip += "\n" + issue; + } } + + if (at(row).isSymLinkUnder(instDirPath())) { + tooltip += + m_resources[row]->internalId() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + } + if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + tooltip += tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } - return m_resources[row]->internal_id(); + return tooltip; + } case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); + if (column == NameColumn) { + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) { + return QIcon::fromTheme("status-bad"); + } + if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) { + return QIcon::fromTheme("status-yellow"); + } + } return {}; } case Qt::CheckStateRole: - if (column == ActiveColumn) + if (column == ActiveColumn) { return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + } return {}; default: return {}; @@ -555,8 +606,9 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { int row = index.row(); - if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) + if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) { return false; + } if (role == Qt::CheckStateRole) { return setResourceEnabled({ index }, EnableAction::TOGGLE); @@ -575,6 +627,7 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien case DateColumn: case ProviderColumn: case SizeColumn: + case FileNameColumn: return columnNames().at(section); default: return {}; @@ -592,6 +645,8 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien return tr("The source provider of the resource."); case SizeColumn: return tr("The size of the resource."); + case FileNameColumn: + return tr("The file name of the resource."); default: return {}; } @@ -612,22 +667,22 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { - auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); - auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); - auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + const auto stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + const auto visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getSetting(stateSettingName); stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false - auto settings = m_instance->settings(); + auto* settings = m_instance->settings(); if (!settings->get(overrideSettingName).toBool()) { settings = APPLICATION->settings(); } auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); - for (auto i = 0; i < m_column_names.size(); ++i) { + for (auto i = 0; i < m_columnNames.size(); ++i) { if (m_columnsHideable[i]) { - auto name = m_column_names[i]; + auto name = m_columnNames[i]; visibility[name] = !tree->isColumnHidden(i); } } @@ -636,24 +691,24 @@ void ResourceFolderModel::saveColumns(QTreeView* tree) void ResourceFolderModel::loadColumns(QTreeView* tree) { - auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); - auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); - auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + const auto stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + const auto visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); - auto setVisible = [this, tree](QVariant value) { + auto setVisible = [this, tree](const QVariant& value) { auto visibility = Json::toMap(value.toString()); - for (auto i = 0; i < m_column_names.size(); ++i) { + for (auto i = 0; i < m_columnNames.size(); ++i) { if (m_columnsHideable[i]) { - auto name = m_column_names[i]; + auto name = m_columnNames[i]; tree->setColumnHidden(i, !visibility.value(name, false).toBool()); } } }; - auto const defaultValue = Json::fromMap({ + const auto defaultValue = Json::fromMap({ { "Image", true }, { "Version", true }, { "Last Modified", true }, @@ -661,7 +716,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) { "Pack Format", true }, }); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false - auto settings = m_instance->settings(); + auto* settings = m_instance->settings(); if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { settings = APPLICATION->settings(); } @@ -670,7 +725,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) // allways connect the signal in case the setting is toggled on and off auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); - connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, QVariant value) { + connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, const QVariant& value) { if (!m_instance->settings()->get(overrideSettingName).toBool()) { setVisible(value); } @@ -679,11 +734,11 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) { - auto menu = new QMenu(tree); + auto* menu = new QMenu(tree); { // action to decide if the visibility is per instance or not - auto act = new QAction(tr("Override Columns Visibility"), menu); - auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto* act = new QAction(tr("Override Columns Visibility"), menu); + const auto overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); act->setCheckable(true); act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); @@ -699,9 +754,10 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) for (int col = 0; col < columnCount(); ++col) { // Skip creating actions for columns that should not be hidden - if (!m_columnsHideable.at(col)) + if (!m_columnsHideable.at(col)) { continue; - auto act = new QAction(menu); + } + auto* act = new QAction(menu); setupHeaderAction(act, col); act->setCheckable(true); @@ -710,8 +766,9 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled) { tree->setColumnHidden(col, !toggled); for (int c = 0; c < columnCount(); ++c) { - if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) + if (m_columnResizeModes.at(c) == QHeaderView::ResizeToContents) { tree->resizeColumnToContents(c); + } } saveColumns(tree); }); @@ -729,41 +786,43 @@ QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* pare SortType ResourceFolderModel::columnToSortKey(size_t column) const { - Q_ASSERT(m_column_sort_keys.size() == columnCount()); - return m_column_sort_keys.at(column); + Q_ASSERT(m_columnSortKeys.size() == columnCount()); + return m_columnSortKeys.at(column); } /* Standard Proxy Model for createFilterProxyModel */ -bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { auto* model = qobject_cast(sourceModel()); - if (!model) + if (!model) { return true; + } - const auto& resource = model->at(source_row); + const auto& resource = model->at(sourceRow); return resource.applyFilter(filterRegularExpression()); } -bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) const { auto* model = qobject_cast(sourceModel()); - if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { - return QSortFilterProxyModel::lessThan(source_left, source_right); + if (!model || !sourceLeft.isValid() || !sourceRight.isValid() || sourceLeft.column() != sourceRight.column()) { + return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight); } // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and // proceed. - auto column_sort_key = model->columnToSortKey(source_left.column()); - auto const& resource_left = model->at(source_left.row()); - auto const& resource_right = model->at(source_right.row()); + auto columnSortKey = model->columnToSortKey(sourceLeft.column()); + const auto& resourceLeft = model->at(sourceLeft.row()); + const auto& resourceRight = model->at(sourceRight.row()); - auto compare_result = resource_left.compare(resource_right, column_sort_key); - if (compare_result == 0) - return QSortFilterProxyModel::lessThan(source_left, source_right); + auto compareResult = resourceLeft.compare(resourceRight, columnSortKey); + if (compareResult == 0) { + return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight); + } - return compare_result < 0; + return compareResult < 0; } QString ResourceFolderModel::instDirPath() const @@ -771,60 +830,69 @@ QString ResourceFolderModel::instDirPath() const return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } -void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) +void ResourceFolderModel::onParseFailed(int ticket, const QString& resourceId) { - auto iter = m_active_parse_tasks.constFind(ticket); - if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + auto iter = m_activeParseTasks.constFind(ticket); + if (iter == m_activeParseTasks.constEnd() || !m_resourcesIndex.contains(resourceId)) { return; + } - auto removed_index = m_resources_index[resource_id]; - auto removed_it = m_resources.begin() + removed_index; - Q_ASSERT(removed_it != m_resources.end()); + auto removedIndex = m_resourcesIndex[resourceId]; + auto removedIt = m_resources.begin() + removedIndex; + Q_ASSERT(removedIt != m_resources.end()); - beginRemoveRows(QModelIndex(), removed_index, removed_index); - m_resources.erase(removed_it); + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + m_resources.erase(removedIt); // update index - m_resources_index.clear(); + m_resourcesIndex.clear(); int idx = 0; - for (auto const& mod : qAsConst(m_resources)) { - m_resources_index[mod->internal_id()] = idx; + for (const auto& mod : qAsConst(m_resources)) { + m_resourcesIndex[mod->internalId()] = idx; idx++; } endRemoveRows(); } -void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) +void ResourceFolderModel::applyUpdates(QSet& currentSet, QSet& newSet, QMap& newResources) { // see if the kept resources changed in some way { - QSet kept_set = current_set; - kept_set.intersect(new_set); + QSet keptSet = currentSet; + keptSet.intersect(newSet); - for (auto const& kept : kept_set) { - auto row_it = m_resources_index.constFind(kept); - Q_ASSERT(row_it != m_resources_index.constEnd()); - auto row = row_it.value(); + for (const auto& kept : keptSet) { + auto rowIt = m_resourcesIndex.constFind(kept); + Q_ASSERT(rowIt != m_resourcesIndex.constEnd()); + auto row = rowIt.value(); - auto& new_resource = new_resources[kept]; - auto const& current_resource = m_resources.at(row); + auto& newResource = newResources[kept]; + const auto& currentResource = m_resources.at(row); - if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { - // no significant change, ignore... + if (newResource->dateTimeChanged() == currentResource->dateTimeChanged()) { + // no significant change + bool hadIssues = !currentResource->hasIssues(); + currentResource->updateIssues(m_instance); + + if (hadIssues != currentResource->hasIssues()) { + emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); + } continue; } // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. - if (current_resource->isResolving()) { - auto ticket = current_resource->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); + if (currentResource->isResolving()) { + auto ticket = currentResource->resolutionTicket(); + if (m_activeParseTasks.contains(ticket)) { + auto* task = (*m_activeParseTasks.find(ticket)).get(); task->abort(); } } - m_resources[row].reset(new_resource); + m_resources[row].reset(newResource); + newResource->updateIssues(m_instance); + resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -832,46 +900,48 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet // remove resources no longer present { - QSet removed_set = current_set; - removed_set.subtract(new_set); + QSet removedSet = currentSet; + removedSet.subtract(newSet); - QList removed_rows; - for (auto& removed : removed_set) - removed_rows.append(m_resources_index[removed]); + QList removedRows; + for (const auto& removed : removedSet) { + removedRows.append(m_resourcesIndex[removed]); + } - std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); + std::ranges::sort(removedRows, std::greater()); - for (auto& removed_index : removed_rows) { - auto removed_it = m_resources.begin() + removed_index; + for (auto& removedIndex : removedRows) { + auto removedIt = m_resources.begin() + removedIndex; - Q_ASSERT(removed_it != m_resources.end()); + Q_ASSERT(removedIt != m_resources.end()); - if ((*removed_it)->isResolving()) { - auto ticket = (*removed_it)->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); + if ((*removedIt)->isResolving()) { + auto ticket = (*removedIt)->resolutionTicket(); + if (m_activeParseTasks.contains(ticket)) { + auto* task = (*m_activeParseTasks.find(ticket)).get(); task->abort(); } } - beginRemoveRows(QModelIndex(), removed_index, removed_index); - m_resources.erase(removed_it); + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + m_resources.erase(removedIt); endRemoveRows(); } } // add new resources to the end { - QSet added_set = new_set; - added_set.subtract(current_set); + QSet addedSet = newSet; + addedSet.subtract(currentSet); // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (added_set.size() > 0) { + if (addedSet.size() > 0) { beginInsertRows(QModelIndex(), static_cast(m_resources.size()), - static_cast(m_resources.size() + added_set.size() - 1)); + static_cast(m_resources.size() + addedSet.size() - 1)); - for (auto& added : added_set) { - auto res = new_resources[added]; + for (const auto& added : addedSet) { + auto res = newResources[added]; + res->updateIssues(m_instance); m_resources.append(res); resolveResource(m_resources.last()); } @@ -882,10 +952,10 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet // update index { - m_resources_index.clear(); + m_resourcesIndex.clear(); int idx = 0; - for (auto const& mod : qAsConst(m_resources)) { - m_resources_index[mod->internal_id()] = idx; + for (const auto& mod : qAsConst(m_resources)) { + m_resourcesIndex[mod->internalId()] = idx; idx++; } } @@ -893,25 +963,29 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet Resource::Ptr ResourceFolderModel::find(QString id) { auto iter = - std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); - if (iter == m_resources.constEnd()) + std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](const Resource::Ptr& r) { return r->internalId() == id; }); + if (iter == m_resources.constEnd()) { return nullptr; + } return *iter; } QList ResourceFolderModel::allResources() { QList result; result.reserve(m_resources.size()); - for (const Resource ::Ptr& resource : m_resources) + for (const Resource ::Ptr& resource : m_resources) { result.append((resource.get())); + } return result; } + QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; for (const QModelIndex& index : indexes) { - if (index.column() != 0) + if (index.column() != 0) { continue; + } result.append(&at(index.row())); } return result; diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index b6343a807..7699394ab 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -61,7 +61,7 @@ class QSortFilterProxyModel; class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: - ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr); ~ResourceFolderModel() override; virtual QString id() const { return "resource"; } @@ -93,13 +93,13 @@ class ResourceFolderModel : public QAbstractListModel { */ virtual bool installResource(QString path); - virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); + virtual void installResourceWithFlameMetadata(const QString& path, ModPlatform::IndexedVersion& vers); /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ - virtual bool uninstallResource(const QString& file_name, bool preserve_metadata = false); + virtual bool uninstallResource(const QString& fileName, bool preserveMetadata = false); virtual bool deleteResources(const QModelIndexList&); virtual void deleteMetadata(const QModelIndexList&); @@ -125,7 +125,7 @@ class ResourceFolderModel : public QAbstractListModel { Resource::Ptr find(QString id); - QDir const& dir() const { return m_dir; } + const QDir& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * @@ -137,12 +137,12 @@ class ResourceFolderModel : public QAbstractListModel { /* Qt behavior */ /* Basic columns */ - enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; + enum Columns : std::uint8_t { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, FileNameColumn, NumColumns }; - QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } + QStringList columnNames(bool translated = true) const { return translated ? m_columnNamesTranslated : m_columnNames; } int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } - int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } + int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NumColumns; } Qt::DropActions supportedDropActions() const override; @@ -153,6 +153,7 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + QBrush rowBackground(int row) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; @@ -170,18 +171,19 @@ class ResourceFolderModel : public QAbstractListModel { QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); SortType columnToSortKey(size_t column) const; - QList columnResizeModes() const { return m_column_resize_modes; } + QList columnResizeModes() const { return m_columnResizeModes; } class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: - bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; - bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight) const override; }; QString instDirPath() const; + BaseInstance* instance() const { return m_instance; } signals: void updateFinished(); @@ -205,7 +207,7 @@ class ResourceFolderModel : public QAbstractListModel { * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ - [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; } + [[nodiscard]] virtual Task* createParseTask(Resource& /*unused*/) { return nullptr; } /** Standard implementation of the model update logic. * @@ -213,10 +215,10 @@ class ResourceFolderModel : public QAbstractListModel { * to act only on those disparities. * */ - void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + void applyUpdates(QSet& currentSet, QSet& newSet, QMap& newResources); protected slots: - void directoryChanged(QString); + void directoryChanged(const QString&); /** Called when the update task is successful. * @@ -232,39 +234,40 @@ class ResourceFolderModel : public QAbstractListModel { * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass * if the resource is complex and has more stuff to parse. */ - virtual void onParseSucceeded(int ticket, QString resource_id); - virtual void onParseFailed(int ticket, QString resource_id); + virtual void onParseSucceeded(int ticket, const QString& resourceId); + virtual void onParseFailed(int ticket, const QString& resourceId); protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; - QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; - QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, true, true, true }; + QList m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Date, + SortType::Provider, SortType::Size, SortType::Filename }; + QStringList m_columnNames = { "Enable", "Name", "Last Modified", "Provider", "Size", "File Name" }; + QStringList m_columnNamesTranslated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") }; + QList m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true, true, true }; QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; - bool m_is_watching = false; + bool m_isWatching = false; - bool m_is_indexed; - bool m_first_folder_load = true; + bool m_isIndexed; + bool m_firstFolderLoad = true; - Task::Ptr m_current_update_task = nullptr; - bool m_scheduled_update = false; + Task::Ptr m_currentUpdateTask = nullptr; + bool m_scheduledUpdate = false; QList m_resources; // Represents the relationship between a resource's internal ID and it's row position on the model. - QMap m_resources_index; + QMap m_resourcesIndex; // Runs off-thread ConcurrentTask m_resourceResolver; bool m_resourceResolverRunning = false; - QMap m_active_parse_tasks; - std::atomic m_next_resolution_ticket = 0; + QMap m_activeParseTasks; + std::atomic m_nextResolutionTicket = 0; }; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index ccf52e8e4..2d4295b1b 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -3,35 +3,101 @@ #include #include #include +#include #include "MTPixmapCache.h" #include "Version.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats -static const QMap> s_pack_format_versions = { - { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, - { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, - { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, - { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, - { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, - { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, - { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, - { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, - { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } }, - { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } }, - { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } }, - { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } }, - { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } }, - { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } }, - { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } }, - { 34, { Version("24w21a"), Version("1.21") } } +static const QMap, std::pair> s_pack_format_versions = { + { { 1, 0 }, { Version("1.6.1"), Version("1.8.9") } }, + { { 2, 0 }, { Version("1.9"), Version("1.10.2") } }, + { { 3, 0 }, { Version("1.11"), Version("1.12.2") } }, + { { 4, 0 }, { Version("1.13"), Version("1.14.4") } }, + { { 5, 0 }, { Version("1.15"), Version("1.16.1") } }, + { { 6, 0 }, { Version("1.16.2"), Version("1.16.5") } }, + { { 7, 0 }, { Version("1.17"), Version("1.17.1") } }, + { { 8, 0 }, { Version("1.18"), Version("1.18.2") } }, + { { 9, 0 }, { Version("1.19"), Version("1.19.2") } }, + { { 11, 0 }, { Version("22w42a"), Version("22w44a") } }, + { { 12, 0 }, { Version("1.19.3"), Version("1.19.3") } }, + { { 13, 0 }, { Version("1.19.4"), Version("1.19.4") } }, + { { 14, 0 }, { Version("23w14a"), Version("23w16a") } }, + { { 15, 0 }, { Version("1.20"), Version("1.20.1") } }, + { { 16, 0 }, { Version("23w31a"), Version("23w31a") } }, + { { 17, 0 }, { Version("23w32a"), Version("1.20.2-pre1") } }, + { { 18, 0 }, { Version("1.20.2"), Version("1.20.2") } }, + { { 19, 0 }, { Version("23w42a"), Version("23w42a") } }, + { { 20, 0 }, { Version("23w43a"), Version("23w44a") } }, + { { 21, 0 }, { Version("23w45a"), Version("23w46a") } }, + { { 22, 0 }, { Version("1.20.3"), Version("1.20.4") } }, + { { 24, 0 }, { Version("24w03a"), Version("24w04a") } }, + { { 25, 0 }, { Version("24w05a"), Version("24w05b") } }, + { { 26, 0 }, { Version("24w06a"), Version("24w07a") } }, + { { 28, 0 }, { Version("24w09a"), Version("24w10a") } }, + { { 29, 0 }, { Version("24w11a"), Version("24w11a") } }, + { { 30, 0 }, { Version("24w12a"), Version("24w12a") } }, + { { 31, 0 }, { Version("24w13a"), Version("1.20.5-pre3") } }, + { { 32, 0 }, { Version("1.20.5"), Version("1.20.6") } }, + { { 33, 0 }, { Version("24w18a"), Version("24w20a") } }, + { { 34, 0 }, { Version("1.21"), Version("1.21.1") } }, + { { 35, 0 }, { Version("24w33a"), Version("24w33a") } }, + { { 36, 0 }, { Version("24w34a"), Version("24w35a") } }, + { { 37, 0 }, { Version("24w36a"), Version("24w36a") } }, + { { 38, 0 }, { Version("24w37a"), Version("24w37a") } }, + { { 39, 0 }, { Version("24w38a"), Version("24w39a") } }, + { { 40, 0 }, { Version("24w40a"), Version("24w40a") } }, + { { 41, 0 }, { Version("1.21.2-pre1"), Version("1.21.2-pre2") } }, + { { 42, 0 }, { Version("1.21.2"), Version("1.21.3") } }, + { { 43, 0 }, { Version("24w44a"), Version("24w44a") } }, + { { 44, 0 }, { Version("24w45a"), Version("24w45a") } }, + { { 45, 0 }, { Version("24w46a"), Version("24w46a") } }, + { { 46, 0 }, { Version("1.21.4"), Version("1.21.4") } }, + { { 47, 0 }, { Version("25w02a"), Version("25w02a") } }, + { { 48, 0 }, { Version("25w03a"), Version("25w03a") } }, + { { 49, 0 }, { Version("25w04a"), Version("25w04a") } }, + { { 50, 0 }, { Version("25w05a"), Version("25w05a") } }, + { { 51, 0 }, { Version("25w06a"), Version("25w06a") } }, + { { 52, 0 }, { Version("25w07a"), Version("25w07a") } }, + { { 53, 0 }, { Version("25w08a"), Version("25w09b") } }, + { { 54, 0 }, { Version("25w10a"), Version("25w10a") } }, + { { 55, 0 }, { Version("1.21.5"), Version("1.21.5") } }, + { { 56, 0 }, { Version("25w15a"), Version("25w15a") } }, + { { 57, 0 }, { Version("25w16a"), Version("25w16a") } }, + { { 58, 0 }, { Version("25w17a"), Version("25w17a") } }, + { { 59, 0 }, { Version("25w18a"), Version("25w18a") } }, + { { 60, 0 }, { Version("25w19a"), Version("25w19a") } }, + { { 61, 0 }, { Version("25w20a"), Version("25w20a") } }, + { { 62, 0 }, { Version("25w21a"), Version("25w21a") } }, + { { 63, 0 }, { Version("1.21.6"), Version("1.21.6") } }, + { { 64, 0 }, { Version("1.21.7"), Version("1.21.8") } }, + { { 65, 0 }, { Version("25w31a"), Version("25w31a") } }, + { { 65, 1 }, { Version("25w32a"), Version("25w32a") } }, + { { 65, 2 }, { Version("25w33a"), Version("25w33a") } }, + { { 66, 0 }, { Version("25w34a"), Version("25w34b") } }, + { { 67, 0 }, { Version("25w35a"), Version("25w35a") } }, + { { 68, 0 }, { Version("25w36a"), Version("25w36b") } }, + { { 69, 0 }, { Version("1.21.9"), Version("1.21.10") } }, + { { 70, 0 }, { Version("25w41a"), Version("25w41a") } }, + { { 70, 1 }, { Version("25w42a"), Version("25w42a") } }, + { { 71, 0 }, { Version("25w43a"), Version("25w43a") } }, + { { 72, 0 }, { Version("25w44a"), Version("25w44a") } }, + { { 73, 0 }, { Version("25w45a"), Version("25w45a") } }, + { { 74, 0 }, { Version("25w46a"), Version("25w46a") } }, + { { 75, 0 }, { Version("1.21.11"), Version("1.21.11") } }, + { { 76, 0 }, { Version("26.1-snap1"), Version("26.1-snap1") } }, + { { 77, 0 }, { Version("26.1-snap2"), Version("26.1-snap2") } }, + { { 78, 0 }, { Version("26.1-snap3"), Version("26.1-snap3") } }, + { { 78, 1 }, { Version("26.1-snap4"), Version("26.1-snap4") } }, + { { 79, 0 }, { Version("26.1-snap5"), Version("26.1-snap5") } }, + { { 80, 0 }, { Version("26.1-snap6"), Version("26.1-snap6") } }, + { { 81, 0 }, { Version("26.1-snap7"), Version("26.1-snap7") } }, + { { 81, 1 }, { Version("26.1-snap8"), Version("26.1-snap9") } }, + { { 82, 0 }, { Version("26.1-snap10"), Version("26.1-snap10") } }, + { { 83, 0 }, { Version("26.1-snap11"), Version("26.1-snap11") } }, }; -std::pair ResourcePack::compatibleVersions() const +QMap, std::pair> ResourcePack::mappings() const { - if (!s_pack_format_versions.contains(m_pack_format)) { - return { {}, {} }; - } - - return s_pack_format_versions.constFind(m_pack_format).value(); + return s_pack_format_versions; } diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 9345e9c27..43aa5e1da 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -22,5 +22,5 @@ class ResourcePack : public DataPack { ResourcePack(QFileInfo file_info) : DataPack(file_info) {} /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - std::pair compatibleVersions() const override; + QMap, std::pair> mappings() const override; }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 5680f4c2d..4dd8e314c 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -39,98 +39,92 @@ #include #include -#include "Version.h" - #include "minecraft/mod/tasks/LocalDataPackParseTask.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) - : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) +ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent) + : ResourceFolderModel(dir, instance, isIndexed, createDir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); - m_column_names_translated = - QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, - SortType::DATE, SortType::PROVIDER, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true, true }; + m_columnNames = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size", "File Name" }); + m_columnNamesTranslated = QStringList( + { tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") }); + m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::PackFormat, + SortType::Date, SortType::Provider, SortType::Size, SortType::Filename }; + m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const { - if (!validateIndex(index)) + if (!validateIndex(index)) { return {}; + } int row = index.row(); int column = index.column(); switch (role) { - case Qt::DisplayRole: - switch (column) { - case NameColumn: - return m_resources[row]->name(); - case PackFormatColumn: { - auto& resource = at(row); - auto pack_format = resource.packFormat(); - if (pack_format == 0) - return tr("Unrecognized"); - - auto version_bounds = resource.compatibleVersions(); - if (version_bounds.first.toString().isEmpty()) - return QString::number(pack_format); - - return QString("%1 (%2 - %3)") - .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); - } - case DateColumn: - return m_resources[row]->dateTimeChanged(); - case ProviderColumn: - return m_resources[row]->provider(); - case SizeColumn: - return m_resources[row]->sizeStr(); - default: - return {}; + case Qt::BackgroundRole: + return rowBackground(row); + case Qt::DisplayRole: { + if (column == PackFormatColumn) { + const auto& resource = at(row); + return resource.packFormatStr(); } + break; + } case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - return m_resources[row]->internal_id(); + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - return {}; + break; default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + case FileNameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn); + break; + default: + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const @@ -145,6 +139,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case ImageColumn: case ProviderColumn: case SizeColumn: + case FileNameColumn: return columnNames().at(section); default: return {}; @@ -165,6 +160,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O return tr("The source provider of the resource pack."); case SizeColumn: return tr("The size of the resource pack."); + case FileNameColumn: + return tr("The file name of the resource pack."); default: return {}; } @@ -180,10 +177,10 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : NUM_COLUMNS; + return parent.isValid() ? 0 : NumColumns; } Task* ResourcePackFolderModel::createParseTask(Resource& resource) { - return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); + return new LocalDataPackParseTask(m_nextResolutionTicket, dynamic_cast(&resource)); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index b552c324e..186bbb75d 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -7,9 +7,19 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; + enum Columns : std::uint8_t { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + ProviderColumn, + SizeColumn, + FileNameColumn, + NumColumns + }; - explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr); QString id() const override { return "resourcepacks"; } @@ -19,7 +29,7 @@ class ResourcePackFolderModel : public ResourceFolderModel { int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } - [[nodiscard]] Task* createParseTask(Resource&) override; + [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override; RESOURCE_HELPERS(ResourcePack) }; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index 9b0180180..ded641770 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -18,7 +18,7 @@ class ShaderPackFolderModel : public ResourceFolderModel { [[nodiscard]] Task* createParseTask(Resource& resource) override { - return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); + return new LocalShaderPackParseTask(m_nextResolutionTicket, static_cast(resource)); } QDir indexDir() const override { return m_dir; } diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 57c5f8ee9..976bf3854 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -36,83 +36,82 @@ #include "TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" -#include "minecraft/mod/tasks/ResourceFolderLoadTask.h" -TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) - : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, isIndexed, createDir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, - QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true }; + m_columnNames = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size", "File Name" }); + m_columnNamesTranslated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("File Name") }); + m_columnSortKeys = { SortType::Enabled, SortType::Name, SortType::Name, SortType::Date, + SortType::Provider, SortType::Size, SortType::Filename }; + m_columnResizeModes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) { - return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); + return new LocalTexturePackParseTask(m_nextResolutionTicket, static_cast(resource)); } QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const { - if (!validateIndex(index)) + if (!validateIndex(index)) { return {}; + } int row = index.row(); int column = index.column(); switch (role) { - case Qt::DisplayRole: - switch (column) { - case NameColumn: - return m_resources[row]->name(); - case DateColumn: - return m_resources[row]->dateTimeChanged(); - case ProviderColumn: - return m_resources[row]->provider(); - case SizeColumn: - return m_resources[row]->sizeStr(); - default: - return {}; - } - case Qt::ToolTipRole: - if (column == NameColumn) { - if (at(row).isSymLinkUnder(instDirPath())) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." - "\nCanonical Path: %1") - .arg(at(row).fileinfo().canonicalFilePath()); - ; - } - if (at(row).isMoreThanOneHardLink()) { - return m_resources[row]->internal_id() + - tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); - } - } - - return m_resources[row]->internal_id(); + case Qt::BackgroundRole: + return rowBackground(row); case Qt::DecorationRole: { - if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } - return {}; + break; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } - return {}; - case Qt::CheckStateRole: - if (column == ActiveColumn) { - return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; - } - return {}; + break; default: - return {}; + break; } + + // map the columns to the base equivilents + QModelIndex mappedIndex; + switch (column) { + case ActiveColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); + break; + case NameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); + break; + case DateColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); + break; + case ProviderColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); + break; + case SizeColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); + break; + case FileNameColumn: + mappedIndex = index.siblingAtColumn(ResourceFolderModel::FileNameColumn); + break; + default: + break; + } + + if (mappedIndex.isValid()) { + return ResourceFolderModel::data(mappedIndex, role); + } + + return {}; } QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const @@ -126,6 +125,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case ImageColumn: case ProviderColumn: case SizeColumn: + case FileNameColumn: return columnNames().at(section); default: return {}; @@ -142,6 +142,8 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or return tr("The source provider of the texture pack."); case SizeColumn: return tr("The size of the texture pack."); + case FileNameColumn: + return tr("The file name of the texture pack."); default: return {}; } @@ -155,5 +157,5 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or int TexturePackFolderModel::columnCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : NUM_COLUMNS; + return parent.isValid() ? 0 : NumColumns; } diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 37f78d8d7..3e7343092 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -44,11 +44,20 @@ class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; + enum Columns : std::uint8_t { + ActiveColumn = 0, + ImageColumn, + NameColumn, + DateColumn, + ProviderColumn, + SizeColumn, + FileNameColumn, + NumColumns + }; - explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool isIndexed, bool createDir, QObject* parent = nullptr); - virtual QString id() const override { return "texturepacks"; } + QString id() const override { return "texturepacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -56,7 +65,7 @@ class TexturePackFolderModel : public ResourceFolderModel { int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } - [[nodiscard]] Task* createParseTask(Resource&) override; + [[nodiscard]] Task* createParseTask(Resource& /*unused*/) override; RESOURCE_HELPERS(TexturePack) }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index f2ab390e9..0859c9880 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -74,13 +74,13 @@ void GetModDependenciesTask::prepare() ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName) { - if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { + if (auto isQuilt = (m_loaderType & ModPlatform::Quilt) != 0U; isQuilt || (m_loaderType & ModPlatform::Fabric) != 0U) { auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](const auto& o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); if (over != overide.cend()) { - return { isQuilt ? over->quilt : over->fabric, dep.type }; + return { .addonId = isQuilt ? over->quilt : over->fabric, .type = dep.type, .version = "" }; } } return dep; @@ -91,40 +91,45 @@ QList GetModDependenciesTask::getDependenciesForVersion { QList c_dependencies; for (auto ver_dep : version.dependencies) { - if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) + if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) { continue; + } ver_dep = getOverride(ver_dep, providerName); auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; }); - dep != c_dependencies.end()) + dep != c_dependencies.end()) { continue; // check the current dependency list + } if (auto dep = std::find_if(m_selected.begin(), m_selected.end(), - [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version : i->pack->addonId == ver_dep.addonId); }); - dep != m_selected.end()) + dep != m_selected.end()) { continue; // check the selected versions + } if (auto dep = std::find_if(m_mods.begin(), m_mods.end(), - [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->provider == providerName && (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId); }); - dep != m_mods.end()) + dep != m_mods.end()) { continue; // check the existing mods + } if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), - [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { + [&ver_dep, providerName, isOnlyVersion](const std::shared_ptr& i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId : i->pack->addonId == ver_dep.addonId); }); - dep != m_pack_dependencies.end()) // check loaded dependencies + dep != m_pack_dependencies.end()) { // check loaded dependencies continue; + } c_dependencies.append(ver_dep); } @@ -134,8 +139,7 @@ QList GetModDependenciesTask::getDependenciesForVersion Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { auto provider = pDep->pack->provider; - auto responseInfo = std::make_shared(); - auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo); + auto [info, responseInfo] = getAPI(provider)->getProject(pDep->pack->addonId.toString()); connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); @@ -181,9 +185,11 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen tasks->addTask(getProjectInfoTask(pDep)); } - ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; + ResourceAPI::DependencySearchArgs args = { + .dependency = dep, .mcVersion = m_version, .loader = m_loaderType, .includeChangelog = true + }; ResourceAPI::Callback callbacks; - callbacks.on_fail = [](QString reason, int) { + callbacks.on_fail = [](const QString& reason, int) { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) { @@ -192,10 +198,10 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), - [dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; }); + [dep, provider](const auto& o) { return o.provider == provider && dep.addonId == o.quilt; }); if (over != overide.cend()) { removePack(dep.addonId); - addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); + addTask(prepareDependencyTask({ .addonId = over->fabric, .type = dep.type, .version = "" }, provider, level)); return; } } @@ -213,7 +219,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { pDep->pack->addonId = pDep->version.addonId; - auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); + auto dep_ = getOverride({ .addonId = pDep->version.addonId, .type = pDep->dependency.type, .version = "" }, provider); if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); addTask(prepareDependencyTask(dep_, provider, level)); @@ -225,7 +231,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen removePack(pDep->version.addonId); return; } - for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) { + for (const auto& dep_ : getDependenciesForVersion(pDep->version, provider)) { addTask(prepareDependencyTask(dep_, provider, level - 1)); } }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 3530d6cc0..d6c2985b0 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" @@ -44,15 +45,11 @@ class GetModDependenciesTask : public SequentialTask { ModPlatform::IndexedPack::Ptr pack; ModPlatform::IndexedVersion version; PackDependency() = default; - PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) - { - pack = p; - version = v; - } + PackDependency(ModPlatform::IndexedPack::Ptr p, ModPlatform::IndexedVersion v) : pack(std::move(p)), version(std::move(v)) {} }; struct PackDependencyExtraInfo { - bool maybe_installed; + bool maybe_installed{}; QStringList required_by; }; @@ -62,12 +59,12 @@ class GetModDependenciesTask : public SequentialTask { QHash getExtraInfo(); private: - inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) + ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) { - if (provider == ModPlatform::ResourceProvider::FLAME) + if (provider == ModPlatform::ResourceProvider::FLAME) { return &m_flameAPI; - else - return &m_modrinthAPI; + } + return &m_modrinthAPI; } protected slots: diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 054ae78c0..1bf9a5c02 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -27,6 +27,7 @@ #include "minecraft/mod/ResourcePack.h" #include +#include namespace DataPackUtils { @@ -163,6 +164,25 @@ bool processZIP(DataPack* pack, ProcessingLevel level) return true; } +std::pair parseVersion(const QJsonValue& value) +{ + if (value.isDouble()) { + // Single integer -> [major, 0] + return std::make_pair(value.toInt(), 0); + } + std::pair version; + if (value.isArray()) { + QJsonArray arr = value.toArray(); + if (arr.size() >= 1) { + version.first = arr.at(0).toInt(); + } + if (arr.size() >= 2) { + version.second = arr.at(1).toInt(); + } + } + return version; +} + // https://minecraft.wiki/w/Data_pack#pack.mcmeta // https://minecraft.wiki/w/Raw_JSON_text_format // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta @@ -177,7 +197,20 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data) try { auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); - pack->setPackFormat(pack_obj["pack_format"].toInt()); + + int pack_format = 0; + std::pair min_format; + std::pair max_format; + if (pack_obj.contains("pack_format")) { + pack_format = pack_obj.value("pack_format").toInt(); + } + if (pack_obj.contains("min_format")) { + min_format = parseVersion(pack_obj.value("min_format")); + } + if (pack_obj.contains("max_format")) { + max_format = parseVersion(pack_obj.value("max_format")); + } + pack->setPackFormat(pack_format, min_format, max_format); pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException:" << e.what() << e.cause(); diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 59d3876b3..74c311676 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -61,6 +61,36 @@ ModDetails ReadMCModInfo(QByteArray contents) for (auto author : authors) { details.authors.append(author.toString()); } + + if (details.mod_id.startsWith("mod_")) { + details.mod_id = details.mod_id.mid(4); + } + + auto addDep = [&details](QString dep) { + if (dep == "mod_MinecraftForge" || dep == "Forge") + return; + if (dep.contains(":")) { + dep = dep.section(":", 1); + } + if (dep.contains("@")) { + dep = dep.section("@", 0, 0); + } + if (dep.startsWith("mod_")) { + dep = dep.mid(4); + } + details.dependencies.append(dep); + }; + + if (firstObj.contains("requiredMods")) { + for (auto dep : firstObj.value("requiredMods").toArray()) { + addDep(dep.toString()); + } + } else if (firstObj.contains("dependencies")) { + for (auto dep : firstObj.value("dependencies").toArray()) { + addDep(dep.toString()); + } + } + return details; }; QJsonParseError jsonError; @@ -198,6 +228,51 @@ ModDetails ReadMCModTOML(QByteArray contents) } details.icon_file = logoFile; + auto parseDep = [&details](toml::array* dependencies) { + static const QStringList ignoreModIds = { "", "forge", "neoforge", "minecraft" }; + if (!dependencies) { + return; + } + auto isNeoForgeDep = [](toml::table* t) { + auto type = (*t)["type"].as_string(); + return type && type->get() == "required"; + }; + auto isForgeDep = [](toml::table* t) { + auto mandatory = (*t)["mandatory"].as_boolean(); + return mandatory && mandatory->get(); + }; + for (auto& dep : *dependencies) { + auto dep_table = dep.as_table(); + if (!dep_table) { + continue; + } + auto modId = (*dep_table)["modId"].as_string(); + if (!modId || ignoreModIds.contains(QString::fromStdString(modId->get()))) { + continue; + } + if (isNeoForgeDep(dep_table) || isForgeDep(dep_table)) { + details.dependencies.append(QString::fromStdString(modId->get())); + } + } + }; + + if (tomlData.contains("dependencies")) { + auto depValue = tomlData["dependencies"]; + if (auto array = depValue.as_array()) { + parseDep(array); + } else if (auto depTable = depValue.as_table()) { + auto expectedKey = details.mod_id.toStdString(); + if (!depTable->contains(expectedKey)) { + if (auto it = depTable->begin(); it != depTable->end()) { + expectedKey = it->first; + } + } + if ((array = (*depTable)[expectedKey].as_array())) { + parseDep(array); + } + } + } + return details; } @@ -276,15 +351,26 @@ ModDetails ReadFabricModInfo(QByteArray contents) details.icon_file = obj.value(key).toString(); } else { // parsing the sizes failed // take the first - for (auto i : obj) { - details.icon_file = i.toString(); - break; + if (auto it = obj.begin(); it != obj.end()) { + details.icon_file = it->toString(); } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } + + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isObject()) { + auto obj = depends.toObject(); + for (auto key : obj.keys()) { + if (key != "fabricloader" && key != "minecraft" && !key.startsWith("fabric-")) { + details.dependencies.append(key); + } + } + } + } } return details; } @@ -363,15 +449,37 @@ ModDetails ReadQuiltModInfo(QByteArray contents) details.icon_file = obj.value(key).toString(); } else { // parsing the sizes failed // take the first - for (auto i : obj) { - details.icon_file = i.toString(); - break; + if (auto it = obj.begin(); it != obj.end()) { + details.icon_file = it->toString(); } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } + if (object.contains("depends")) { + auto depends = object.value("depends"); + if (depends.isArray()) { + auto array = depends.toArray(); + for (auto obj : array) { + QString modId; + if (obj.isString()) { + modId = obj.toString(); + } else if (obj.isObject()) { + auto objValue = obj.toObject(); + modId = objValue.value("id").toString(); + if (objValue.contains("optional") && objValue.value("optional").toBool()) { + continue; + } + } else { + continue; + } + if (modId != "minecraft" && !modId.startsWith("quilt_")) { + details.dependencies.append(modId); + } + } + } + } } } catch (const Exception& e) { @@ -460,7 +568,7 @@ bool process(Mod& mod, ProcessingLevel level) case ResourceType::LITEMOD: return processLitemod(mod); default: - qWarning() << "Invalid type for mod parse task!"; + qWarning() << "Invalid type" << mod.type() << "for mod parse task!"; return false; } } @@ -643,7 +751,7 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap) if (icon_info.exists() && icon_info.isFile()) { QFile icon(icon_info.filePath()); if (!icon.open(QIODevice::ReadOnly)) { - return png_invalid("failed to open file " + icon_info.filePath()); + return png_invalid("failed to open file " + icon_info.filePath() + " " + icon.errorString()); } auto data = icon.readAll(); diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 3b98e053b..a90e9ca5e 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -41,26 +41,28 @@ #include "minecraft/mod/MetadataHandler.h" #include +#include -ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, - const QDir& index_dir, - bool is_indexed, - bool clean_orphan, - std::function create_function) +ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resourceDir, + const QDir& indexDir, + bool isIndexed, + bool cleanOrphan, + std::function createFunction) : Task(false) - , m_resource_dir(resource_dir) - , m_index_dir(index_dir) - , m_is_indexed(is_indexed) - , m_clean_orphan(clean_orphan) - , m_create_func(create_function) + , m_resource_dir(resourceDir) + , m_index_dir(indexDir) + , m_is_indexed(isIndexed) + , m_clean_orphan(cleanOrphan) + , m_create_func(std::move(createFunction)) , m_result(new Result()) , m_thread_to_spawn_into(thread()) {} void ResourceFolderLoadTask::executeTask() { - if (thread() != m_thread_to_spawn_into) + if (thread() != m_thread_to_spawn_into) { connect(this, &Task::finished, this->thread(), &QThread::quit); + } if (m_is_indexed) { // Read metadata first @@ -71,7 +73,7 @@ void ResourceFolderLoadTask::executeTask() m_resource_dir.refresh(); for (auto entry : m_resource_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); - if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + if (auto* app = APPLICATION_DYN; (app != nullptr) && app->checkQSavePath(filePath)) { continue; } auto newFilePath = FS::getUniqueResourceName(filePath); @@ -83,29 +85,29 @@ void ResourceFolderLoadTask::executeTask() Resource* resource = m_create_func(entry); if (resource->enabled()) { - if (m_result->resources.contains(resource->internal_id())) { - m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + if (m_result->resources.contains(resource->internalId())) { + m_result->resources[resource->internalId()]->setStatus(ResourceStatus::Installed); // Delete the object we just created, since a valid one is already in the mods list. delete resource; } else { - m_result->resources[resource->internal_id()].reset(resource); - m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + m_result->resources[resource->internalId()].reset(resource); + m_result->resources[resource->internalId()]->setStatus(ResourceStatus::NoMetadata); } } else { - QString chopped_id = resource->internal_id().chopped(9); - if (m_result->resources.contains(chopped_id)) { - m_result->resources[resource->internal_id()].reset(resource); + QString choppedId = resource->internalId().chopped(9); + if (m_result->resources.contains(choppedId)) { + m_result->resources[resource->internalId()].reset(resource); - auto metadata = m_result->resources[chopped_id]->metadata(); + auto metadata = m_result->resources[choppedId]->metadata(); if (metadata) { resource->setMetadata(*metadata); - m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); - m_result->resources.remove(chopped_id); + m_result->resources[resource->internalId()]->setStatus(ResourceStatus::Installed); + m_result->resources.remove(choppedId); } } else { - m_result->resources[resource->internal_id()].reset(resource); - m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + m_result->resources[resource->internalId()].reset(resource); + m_result->resources[resource->internalId()]->setStatus(ResourceStatus::NoMetadata); } } } @@ -116,38 +118,41 @@ void ResourceFolderLoadTask::executeTask() QMutableMapIterator iter(m_result->resources); while (iter.hasNext()) { auto resource = iter.next().value(); - if (resource->status() == ResourceStatus::NOT_INSTALLED) { + if (resource->status() == ResourceStatus::NotInstalled) { resource->destroy(m_index_dir, false, false); iter.remove(); } } } - for (auto mod : m_result->resources) + for (const auto& mod : m_result->resources) { mod->moveToThread(m_thread_to_spawn_into); + } - if (m_aborted) + if (m_aborted) { emit finished(); - else + } else { emitSucceeded(); + } } void ResourceFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); - for (auto entry : m_index_dir.entryList(QDir::Files)) { + for (const auto& entry : m_index_dir.entryList(QDir::Files)) { if (!entry.endsWith(".pw.toml")) { continue; } auto metadata = Metadata::get(m_index_dir, entry); - if (!metadata.isValid()) + if (!metadata.isValid()) { continue; + } auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); resource->setMetadata(metadata); - resource->setStatus(ResourceStatus::NOT_INSTALLED); - m_result->resources[resource->internal_id()].reset(resource); + resource->setStatus(ResourceStatus::NotInstalled); + m_result->resources[resource->internalId()].reset(resource); } } diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 7c872c13d..6489176b7 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -41,7 +41,7 @@ #include #include #include -#include "minecraft/mod/Mod.h" +#include "minecraft/mod/Resource.h" #include "tasks/Task.h" class ResourceFolderLoadTask : public Task { @@ -54,11 +54,11 @@ class ResourceFolderLoadTask : public Task { ResultPtr result() const { return m_result; } public: - ResourceFolderLoadTask(const QDir& resource_dir, - const QDir& index_dir, - bool is_indexed, - bool clean_orphan, - std::function create_function); + ResourceFolderLoadTask(const QDir& resourceDir, + const QDir& indexDir, + bool isIndexed, + bool cleanOrphan, + std::function createFunction); bool canAbort() const override { return true; } bool abort() override @@ -76,7 +76,7 @@ class ResourceFolderLoadTask : public Task { QDir m_resource_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; - std::function m_create_func; + std::function m_create_func; ResultPtr m_result; std::atomic m_aborted = false; diff --git a/launcher/minecraft/skins/CapeChange.cpp b/launcher/minecraft/skins/CapeChange.cpp index abbaa0b67..c955a1622 100644 --- a/launcher/minecraft/skins/CapeChange.cpp +++ b/launcher/minecraft/skins/CapeChange.cpp @@ -36,9 +36,8 @@ #include "CapeChange.h" +#include #include - -#include "net/ByteArraySink.h" #include "net/RawHeaderProxy.h" CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) @@ -62,8 +61,8 @@ CapeChange::Ptr CapeChange::make(QString token, QString capeId) auto up = makeShared(capeId); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); - up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); - up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + up->m_sink.reset(new Net::DummySink()); + up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; diff --git a/launcher/minecraft/skins/SkinDelete.cpp b/launcher/minecraft/skins/SkinDelete.cpp index 94aca62ca..9c98e3faa 100644 --- a/launcher/minecraft/skins/SkinDelete.cpp +++ b/launcher/minecraft/skins/SkinDelete.cpp @@ -36,7 +36,7 @@ #include "SkinDelete.h" -#include "net/ByteArraySink.h" +#include #include "net/RawHeaderProxy.h" SkinDelete::SkinDelete() : NetRequest() @@ -54,8 +54,8 @@ SkinDelete::Ptr SkinDelete::make(QString token) { auto up = makeShared(); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); - up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); - up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + up->m_sink.reset(new Net::DummySink()); + up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index b47f17db5..c960b9493 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -121,7 +121,7 @@ bool SkinList::update() auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... for (QFileInfo entry : folderContents) { - if (!entry.isFile() && entry.suffix() != "png") + if (!entry.isFile() || entry.suffix() != "png") continue; SkinModel w(entry.absoluteFilePath()); diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index ca2545e05..e2c41f17b 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -58,7 +58,7 @@ static QImage improveSkin(QImage skin) // It seems some older skins may use this format, which can't be drawn onto // https://github.com/PrismLauncher/PrismLauncher/issues/4032 // https://doc.qt.io/qt-6/qpainter.html#begin - if (skin.format() == QImage::Format_Indexed8) { + if (skin.format() <= QImage::Format_Indexed8 || !skin.hasAlphaChannel()) { skin = skin.convertToFormat(QImage::Format_ARGB32); } diff --git a/launcher/minecraft/skins/SkinUpload.cpp b/launcher/minecraft/skins/SkinUpload.cpp index ccc29d281..8399f1f7d 100644 --- a/launcher/minecraft/skins/SkinUpload.cpp +++ b/launcher/minecraft/skins/SkinUpload.cpp @@ -39,7 +39,7 @@ #include #include "FileSystem.h" -#include "net/ByteArraySink.h" +#include "net/DummySink.h" #include "net/RawHeaderProxy.h" SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) @@ -72,8 +72,8 @@ SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) auto up = makeShared(path, variant); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); - up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); - up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + up->m_sink.reset(new Net::DummySink()); + up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index bfce11151..22eea47b2 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -39,7 +39,7 @@ void AssetUpdateTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); - connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); @@ -73,7 +73,7 @@ void AssetUpdateTask::assetIndexFinished() auto job = index.getDownloadJob(); if (job) { - QString resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + QString resourceURL = resourceUrl(); QString source = tr("Mojang"); if (resourceURL != BuildConfig.DEFAULT_RESOURCE_BASE) { source = QUrl(resourceURL).host(); @@ -82,7 +82,7 @@ void AssetUpdateTask::assetIndexFinished() downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); - connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); downloadJob->start(); @@ -111,3 +111,12 @@ bool AssetUpdateTask::abort() } return true; } + +QString AssetUpdateTask::resourceUrl() +{ + if (const QString urlOverride = APPLICATION->settings()->get("ResourceURLOverride").toString(); !urlOverride.isEmpty()) { + return urlOverride; + } + + return BuildConfig.DEFAULT_RESOURCE_BASE; +} diff --git a/launcher/minecraft/update/AssetUpdateTask.h b/launcher/minecraft/update/AssetUpdateTask.h index 88fac0ac5..56aecc293 100644 --- a/launcher/minecraft/update/AssetUpdateTask.h +++ b/launcher/minecraft/update/AssetUpdateTask.h @@ -13,6 +13,9 @@ class AssetUpdateTask : public Task { bool canAbort() const override; + public: + static QString resourceUrl(); + private slots: void assetIndexFinished(); void assetIndexFailed(QString reason); diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/LegacyFMLLibrariesTask.cpp similarity index 73% rename from launcher/minecraft/update/FMLLibrariesTask.cpp rename to launcher/minecraft/update/LegacyFMLLibrariesTask.cpp index ce0c9a723..a3bcc145f 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/LegacyFMLLibrariesTask.cpp @@ -1,4 +1,4 @@ -#include "FMLLibrariesTask.h" +#include "LegacyFMLLibrariesTask.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" @@ -10,11 +10,11 @@ #include "net/ApiDownload.h" -FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance* inst) +LegacyFMLLibrariesTask::LegacyFMLLibrariesTask(MinecraftInstance* inst) { m_inst = inst; } -void FMLLibrariesTask::executeTask() +void LegacyFMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance* inst = (MinecraftInstance*)m_inst; @@ -61,27 +61,28 @@ void FMLLibrariesTask::executeTask() NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; auto metacache = APPLICATION->metacache(); Net::Download::Options options = Net::Download::Option::MakeEternal; + const QString base = baseUrl(); for (auto& lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; + QString urlString = base + lib.filename; dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); } - connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); - connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); - connect(dljob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); - connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); - connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress); + connect(dljob.get(), &NetJob::succeeded, this, &LegacyFMLLibrariesTask::fmllibsFinished); + connect(dljob.get(), &NetJob::failed, this, &LegacyFMLLibrariesTask::fmllibsFailed); + connect(dljob.get(), &NetJob::aborted, this, &LegacyFMLLibrariesTask::emitAborted); + connect(dljob.get(), &NetJob::progress, this, &LegacyFMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::stepProgress, this, &LegacyFMLLibrariesTask::propagateStepProgress); downloadJob.reset(dljob); downloadJob->start(); } -bool FMLLibrariesTask::canAbort() const +bool LegacyFMLLibrariesTask::canAbort() const { return true; } -void FMLLibrariesTask::fmllibsFinished() +void LegacyFMLLibrariesTask::fmllibsFinished() { downloadJob.reset(); if (!fmlLibsToProcess.isEmpty()) { @@ -107,19 +108,28 @@ void FMLLibrariesTask::fmllibsFinished() } emitSucceeded(); } -void FMLLibrariesTask::fmllibsFailed(QString reason) +void LegacyFMLLibrariesTask::fmllibsFailed(QString reason) { QStringList failed = downloadJob->getFailedFiles(); QString failed_all = failed.join("\n"); emitFailed(tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); } -bool FMLLibrariesTask::abort() +bool LegacyFMLLibrariesTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { - qWarning() << "Prematurely aborted FMLLibrariesTask"; + qWarning() << "Prematurely aborted LegacyFMLLibrariesTask"; } return true; } + +QString LegacyFMLLibrariesTask::baseUrl() +{ + if (const QString urlOverride = APPLICATION->settings()->get("LegacyFMLLibsURLOverride").toString(); !urlOverride.isEmpty()) { + return urlOverride; + } + + return BuildConfig.LEGACY_FMLLIBS_BASE_URL; +} diff --git a/launcher/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/LegacyFMLLibrariesTask.h similarity index 71% rename from launcher/minecraft/update/FMLLibrariesTask.h rename to launcher/minecraft/update/LegacyFMLLibrariesTask.h index 4fe2648e8..2591f7c9f 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.h +++ b/launcher/minecraft/update/LegacyFMLLibrariesTask.h @@ -5,11 +5,11 @@ class MinecraftInstance; -class FMLLibrariesTask : public Task { +class LegacyFMLLibrariesTask : public Task { Q_OBJECT public: - FMLLibrariesTask(MinecraftInstance* inst); - virtual ~FMLLibrariesTask() = default; + LegacyFMLLibrariesTask(MinecraftInstance* inst); + virtual ~LegacyFMLLibrariesTask() = default; void executeTask() override; @@ -22,6 +22,9 @@ class FMLLibrariesTask : public Task { public slots: bool abort() override; + private: + static QString baseUrl(); + private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index e64691d51..f725af18d 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -31,7 +31,7 @@ void LibrariesTask::executeTask() emitFailed(tr("Null jar is specified in the metadata, aborting.")); return false; } - auto dls = lib->getDownloads(inst->runtimeContext(), metacache.get(), errors, localPath); + auto dls = lib->getDownloads(inst->runtimeContext(), metacache, errors, localPath); for (auto dl : dls) { downloadJob->addNetAction(dl); } @@ -44,8 +44,8 @@ void LibrariesTask::executeTask() libArtifactPool.append(profile->getLibraries()); libArtifactPool.append(profile->getNativeLibraries()); libArtifactPool.append(profile->getMavenFiles()); - for (auto agent : profile->getAgents()) { - libArtifactPool.append(agent->library()); + for (const auto& agent : profile->getAgents()) { + libArtifactPool.append(agent.library); } libArtifactPool.append(profile->getMainJar()); processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); @@ -64,7 +64,7 @@ void LibrariesTask::executeTask() connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); - connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); + connect(downloadJob.get(), &NetJob::aborted, this, &LibrariesTask::emitAborted); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index c5beff26c..ad68fd963 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -12,22 +12,18 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(QList& resources, - std::list& mcVersions, + std::vector& mcVersions, QList loadersList, - std::shared_ptr resourceModel) - : Task() - , m_resources(resources) - , m_gameVersions(mcVersions) - , m_loadersList(std::move(loadersList)) - , m_resourceModel(std::move(resourceModel)) + ResourceFolderModel* resourceModel) + : m_resources(resources), m_gameVersions(mcVersions), m_loadersList(std::move(loadersList)), m_resourceModel(resourceModel) {} struct Update { QString name; - QString old_hash; - QString old_version; - QString new_version; - std::optional new_version_type; + QString oldHash; + QString oldVersion; + QString newVersion; + std::optional newVersionType; QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; @@ -35,19 +31,19 @@ class CheckUpdateTask : public Task { public: Update(QString name, - QString old_h, - QString old_v, - QString new_v, - std::optional new_v_type, + QString oldH, + QString oldV, + QString newV, + std::optional newVType, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t, bool enabled = true) : name(std::move(name)) - , old_hash(std::move(old_h)) - , old_version(std::move(old_v)) - , new_version(std::move(new_v)) - , new_version_type(std::move(new_v_type)) + , oldHash(std::move(oldH)) + , oldVersion(std::move(oldV)) + , newVersion(std::move(newV)) + , newVersionType(newVType) , changelog(std::move(changelog)) , provider(p) , download(std::move(t)) @@ -58,20 +54,17 @@ class CheckUpdateTask : public Task { auto getUpdates() -> std::vector&& { return std::move(m_updates); } auto getDependencies() -> QList>&& { return std::move(m_deps); } - public slots: - bool abort() override = 0; - protected slots: void executeTask() override = 0; signals: - void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); + void checkFailed(Resource* failed, QString reason, QUrl recoverUrl = {}); protected: QList& m_resources; - std::list& m_gameVersions; + std::vector& m_gameVersions; QList m_loadersList; - std::shared_ptr m_resourceModel; + ResourceFolderModel* m_resourceModel; std::vector m_updates; QList> m_deps; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 3ade13481..2c7e485e5 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -99,7 +99,7 @@ void EnsureMetadataTask::executeTask() } // They already have the right metadata :o - if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { + if (resource->status() != ResourceStatus::NoMetadata && resource->metadata() && resource->metadata()->provider == m_provider) { qDebug() << "Resource" << resource->name() << "already has metadata!"; emitReady(resource); continue; @@ -215,8 +215,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); - auto response = std::make_shared(); - auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response); + auto [ver_task, response] = modrinth_api.currentVersions(m_resources.keys(), hash_type); // Prevents unfortunate timings when aborting the task if (!ver_task) @@ -264,18 +263,18 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; - for (auto const& data : m_tempVersions) + for (const auto& data : m_tempVersions) addonIds.insert(data.addonId.toString(), data.hash); - auto response = std::make_shared(); Task::Ptr proj_task; + QByteArray* response; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { - proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); + std::tie(proj_task, response) = modrinth_api.getProject(*addonIds.keyBegin()); } else { - proj_task = modrinth_api.getProjects(addonIds.keys(), response); + std::tie(proj_task, response) = modrinth_api.getProjects(addonIds.keys()); } // Prevents unfortunate timings when aborting the task @@ -341,14 +340,12 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() // Flame Task::Ptr EnsureMetadataTask::flameVersionsTask() { - auto response = std::make_shared(); - QList fingerprints; for (auto& murmur : m_resources.keys()) { fingerprints.push_back(murmur.toUInt()); } - auto ver_task = flame_api.matchFingerprints(fingerprints, response); + auto [ver_task, response] = flame_api.matchFingerprints(fingerprints); connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; @@ -407,7 +404,7 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; - for (auto const& hash : m_resources.keys()) { + for (const auto& hash : m_resources.keys()) { if (m_tempVersions.contains(hash)) { auto data = m_tempVersions.find(hash).value(); @@ -417,15 +414,15 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() } } - auto response = std::make_shared(); Task::Ptr proj_task; + QByteArray* response; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { - proj_task = flame_api.getProject(*addonIds.keyBegin(), response); + std::tie(proj_task, response) = flame_api.getProject(*addonIds.keyBegin()); } else { - proj_task = flame_api.getProjects(addonIds.keys(), response); + std::tie(proj_task, response) = flame_api.getProjects(addonIds.keys()); } // Prevents unfortunate timings when aborting the task diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 7b20d37ec..635b2ae92 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -25,6 +25,11 @@ namespace ModPlatform { +ModLoaderType operator|(ModLoaderType lhs, ModLoaderType rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + static const QMap s_indexed_version_type_names = { { "release", IndexedVersionType::Release }, { "beta", IndexedVersionType::Beta }, { "alpha", IndexedVersionType::Alpha } }; @@ -178,4 +183,40 @@ Side SideUtils::fromString(QString side) return Side::UniversalSide; return Side::UniversalSide; } + +QString DependencyTypeUtils::toString(DependencyType type) +{ + switch (type) { + case DependencyType::REQUIRED: + return "REQUIRED"; + case DependencyType::OPTIONAL: + return "OPTIONAL"; + case DependencyType::INCOMPATIBLE: + return "INCOMPATIBLE"; + case DependencyType::EMBEDDED: + return "EMBEDDED"; + case DependencyType::TOOL: + return "TOOL"; + case DependencyType::INCLUDE: + return "INCLUDE"; + case DependencyType::UNKNOWN: + return "UNKNOWN"; + } + return "UNKNOWN"; +} + +DependencyType DependencyTypeUtils::fromString(const QString& str) +{ + static const QHash map = { + { "REQUIRED", DependencyType::REQUIRED }, + { "OPTIONAL", DependencyType::OPTIONAL }, + { "INCOMPATIBLE", DependencyType::INCOMPATIBLE }, + { "EMBEDDED", DependencyType::EMBEDDED }, + { "TOOL", DependencyType::TOOL }, + { "INCLUDE", DependencyType::INCLUDE }, + { "UNKNOWN", DependencyType::UNKNOWN }, + }; + + return map.value(str.toUpper(), DependencyType::UNKNOWN); +} } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 5cde2274f..8984e575e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -24,40 +24,52 @@ #include #include #include +#include #include class QIODevice; namespace ModPlatform { -enum ModLoaderType { - NeoForge = 1 << 0, - Forge = 1 << 1, - Cauldron = 1 << 2, - LiteLoader = 1 << 3, - Fabric = 1 << 4, - Quilt = 1 << 5, - DataPack = 1 << 6, - Babric = 1 << 7, - BTA = 1 << 8, - LegacyFabric = 1 << 9, - Ornithe = 1 << 10, - Rift = 1 << 11 +enum class ModLoaderType : std::uint16_t { + None = 0U, + NeoForge = 1U << 0U, + Forge = 1U << 1U, + Cauldron = 1U << 2U, + LiteLoader = 1U << 3U, + Fabric = 1U << 4U, + Quilt = 1U << 5U, + DataPack = 1U << 6U, + Babric = 1U << 7U, + BTA = 1U << 8U, + LegacyFabric = 1U << 9U, + Ornithe = 1U << 10U, + Rift = 1U << 11U }; + +ModLoaderType operator|(ModLoaderType lhs, ModLoaderType rhs); + +using enum ModLoaderType; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); -enum class ResourceProvider { MODRINTH, FLAME }; +enum class ResourceProvider : std::uint8_t { MODRINTH, FLAME }; -enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; +enum class DependencyType : std::uint8_t { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; -enum class Side { NoSide = 0, ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; +enum class Side : std::uint8_t { NoSide = 0, ClientSide = 1U << 0U, ServerSide = 1U << 1U, UniversalSide = ClientSide | ServerSide }; namespace SideUtils { QString toString(Side side); Side fromString(QString side); } // namespace SideUtils +namespace DependencyTypeUtils { +QString toString(DependencyType type); +DependencyType fromString(const QString& str); +} // namespace DependencyTypeUtils + namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); @@ -76,11 +88,11 @@ struct DonationData { }; struct IndexedVersionType { - enum class Enum { Unknown, Release = 1, Beta, Alpha }; + enum class Enum : std::uint8_t { Unknown = 0, Release = 1, Beta = 2, Alpha = 3 }; using enum Enum; - constexpr IndexedVersionType(Enum e = Unknown) : m_type(e) {} + constexpr IndexedVersionType(Enum e = Unknown) : m_type(e) {} // NOLINT(hicpp-explicit-conversions) static IndexedVersionType fromString(const QString& type); - inline bool isValid() const { return m_type != Unknown; } + bool isValid() const { return m_type != Unknown; } std::strong_ordering operator<=>(const IndexedVersionType& other) const = default; std::strong_ordering operator<=>(const IndexedVersionType::Enum& other) const { return m_type <=> other; } QString toString() const; @@ -101,19 +113,19 @@ struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; - QString version_number = {}; + QString version_number; IndexedVersionType version_type; QStringList mcVersion; QString downloadUrl; QString date; QString fileName; - ModLoaderTypes loaders = {}; + ModLoaderTypes loaders; QString hash_type; QString hash; bool is_preferred = true; QString changelog; QList dependencies; - Side side; // this is for flame API + Side side = Side::NoSide; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; @@ -123,7 +135,7 @@ struct IndexedVersion { auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : ""; auto versionStr = !version.contains(version_number) ? version_number : ""; QString gameVersion = ""; - for (auto v : mcVersion) { + for (const auto& v : mcVersion) { if (version.contains(v)) { gameVersion = ""; break; @@ -161,7 +173,7 @@ struct IndexedPack { QString logoName; QString logoUrl; QString websiteUrl; - Side side; + Side side = Side::NoSide; bool versionsLoaded = false; QList versions; @@ -173,17 +185,19 @@ struct IndexedPack { // For internal use, not provided by APIs bool isVersionSelected(int index) const { - if (!versionsLoaded) + if (!versionsLoaded) { return false; + } return versions.at(index).is_currently_selected; } bool isAnyVersionSelected() const { - if (!versionsLoaded) + if (!versionsLoaded) { return false; + } - return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); + return std::any_of(versions.constBegin(), versions.constEnd(), [](const auto& v) { return v.is_currently_selected; }); } }; @@ -196,11 +210,13 @@ struct OverrideDep { inline auto getOverrideDeps() -> QList { - return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME }, - { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME }, + return { + { .quilt = "634179", .fabric = "306612", .slug = "API", .provider = ModPlatform::ResourceProvider::FLAME }, + { .quilt = "720410", .fabric = "308769", .slug = "KotlinLibraries", .provider = ModPlatform::ResourceProvider::FLAME }, - { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, - { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; + { .quilt = "qvIfYCYJ", .fabric = "P7dR8mSH", .slug = "API", .provider = ModPlatform::ResourceProvider::MODRINTH }, + { .quilt = "lwVhp9o5", .fabric = "Ha28R6CL", .slug = "KotlinLibraries", .provider = ModPlatform::ResourceProvider::MODRINTH } + }; } QString getMetaURL(ResourceProvider provider, QVariant projectID); @@ -210,8 +226,8 @@ auto getModLoaderFromString(QString type) -> ModLoaderType; constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept { - auto x = static_cast(l); - return x && !(x & (x - 1)); + auto x = static_cast(l); + return (x != 0U) && ((x & (x - 1U)) == 0U); } struct Category { diff --git a/launcher/modplatform/ResourceAPI.cpp b/launcher/modplatform/ResourceAPI.cpp index d3a4d8e31..bd683ed21 100644 --- a/launcher/modplatform/ResourceAPI.cpp +++ b/launcher/modplatform/ResourceAPI.cpp @@ -18,10 +18,10 @@ Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(search_url)); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; @@ -84,9 +84,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback(QString("%1::Versions").arg(args.pack->name), APPLICATION->network()); - auto response = std::make_shared(); - netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); + auto [action, response] = Net::ApiDownload::makeByteArray(versions_url); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; @@ -106,11 +106,13 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, CallbackaddonId; + } - if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) { // Heuristic to check if the returned value is valid unsortedVersions.append(file); + } } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -146,10 +148,9 @@ Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback&& callbacks) const +Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks, bool askRetry) const { - auto response = std::make_shared(); - auto job = getProject(args.pack->addonId.toString(), response); + auto [job, response] = getProject(args.pack->addonId.toString(), askRetry); QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { auto pack = args.pack; @@ -204,9 +205,8 @@ Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callbac auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); - auto response = std::make_shared(); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); + auto [action, response] = Net::ApiDownload::makeByteArray(versions_url); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; @@ -262,7 +262,7 @@ Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callbac return netJob; } -QString ResourceAPI::getGameVersionsString(std::list mcVersions) const +QString ResourceAPI::getGameVersionsString(std::vector mcVersions) const { QString s; for (auto& ver : mcVersions) { @@ -284,17 +284,19 @@ QString ResourceAPI::mapMCVersionToModrinth(Version v) const return verStr; } -Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr response) const +std::pair ResourceAPI::getProject(QString addonId, bool askRetry) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) - return nullptr; + return { nullptr, nullptr }; auto project_url = project_url_optional.value(); auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + netJob->setAskRetry(askRetry); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(project_url)); + netJob->addNetAction(action); - return netJob; + return { netJob, response }; } diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index ca77dc8a7..51b6d4b50 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -44,6 +44,7 @@ #include #include +#include #include "../Version.h" @@ -70,7 +71,7 @@ class ResourceAPI { template struct Callback { std::function on_succeed; - std::function on_fail; + std::function on_fail; std::function on_abort; }; @@ -81,18 +82,19 @@ class ResourceAPI { std::optional search; std::optional sorting; std::optional loaders; - std::optional> versions; + std::optional> versions; std::optional side; std::optional categoryIds; - bool openSource; + bool openSource{}; }; struct VersionSearchArgs { ModPlatform::IndexedPack::Ptr pack; - std::optional> mcVersions; + std::optional> mcVersions; std::optional loaders; ModPlatform::ResourceType resourceType; + bool includeChangelog{}; }; struct ProjectInfoArgs { @@ -103,6 +105,7 @@ class ResourceAPI { ModPlatform::Dependency dependency; Version mcVersion; ModPlatform::ModLoaderTypes loader; + bool includeChangelog{}; }; public: @@ -112,10 +115,10 @@ class ResourceAPI { public slots: virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; - virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const; - virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const = 0; + virtual std::pair getProject(QString addonId, bool askRetry = true) const; + virtual std::pair getProjects(QStringList addonIds) const = 0; - virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&) const; + virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&, bool askRetry = true) const; Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const; virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; @@ -124,13 +127,13 @@ class ResourceAPI { QString mapMCVersionToModrinth(Version v) const; - QString getGameVersionsString(std::list mcVersions) const; + QString getGameVersionsString(std::vector mcVersions) const; public: - virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; - virtual auto getInfoURL(QString const& id) const -> std::optional = 0; - virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; - virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; + virtual auto getSearchURL(const SearchArgs& args) const -> std::optional = 0; + virtual auto getInfoURL(const QString& id) const -> std::optional = 0; + virtual auto getVersionsURL(const VersionSearchArgs& args) const -> std::optional = 0; + virtual auto getDependencyURL(const DependencySearchArgs& args) const -> std::optional = 0; /** Functions to load data into a pack. * diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 851473365..f3f66997b 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -38,6 +38,7 @@ #include #include +#include #include "FileSystem.h" #include "Json.h" @@ -59,18 +60,28 @@ #include "BuildConfig.h" #include "ui/dialogs/BlockedModsDialog.h" +namespace { +bool isPathTraversal(const QString& basePath, const QString& entryName) +{ + auto safeName = FS::RemoveInvalidPathChars(entryName); + auto fullPath = FS::PathCombine(basePath, safeName); + auto baseUrl = QUrl::fromLocalFile(basePath); + return !baseUrl.isParentOf(QUrl::fromLocalFile(fullPath)); +} + +Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) +{ + return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); +} +} // namespace + namespace ATLauncher { -static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version); - PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode) + : m_support(support), m_install_mode(installMode), m_pack_name(packName), m_version_name(std::move(version)) { - m_support = support; - m_pack_name = packName; static const QRegularExpression s_regex("[^A-Za-z0-9]"); m_pack_safe_name = packName.replace(s_regex, ""); - m_version_name = version; - m_install_mode = installMode; } bool PackInstallTask::abort() @@ -87,9 +98,11 @@ void PackInstallTask::executeTask() NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); - connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(searchUrl)); + netJob->addNetAction(action); + + connect(netJob.get(), &NetJob::succeeded, this, [this, response] { onDownloadSucceeded(response); }); connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); @@ -97,17 +110,19 @@ void PackInstallTask::executeTask() jobPtr->start(); } -void PackInstallTask::onDownloadSucceeded() +void PackInstallTask::onDownloadSucceeded(QByteArray* responsePtr) { qDebug() << "PackInstallTask::onDownloadSucceeded:" << QThread::currentThreadId(); + + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); jobPtr.reset(); - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ATLauncher at" << parse_error.offset - << "reason:" << parse_error.errorString(); - qWarning() << *response.get(); + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATLauncher at" << parseError.offset << "reason:" << parseError.errorString(); + qWarning() << response; return; } auto obj = doc.object(); @@ -123,7 +138,7 @@ void PackInstallTask::onDownloadSucceeded() // Derived from the installation mode QString message; - bool resetDirectory; + bool resetDirectory = false; switch (m_install_mode) { case InstallMode::Reinstall: @@ -143,8 +158,9 @@ void PackInstallTask::onDownloadSucceeded() } // Display message if one exists - if (!message.isEmpty()) + if (!message.isEmpty()) { m_support->displayMessage(message); + } auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { @@ -168,7 +184,7 @@ void PackInstallTask::onDownloadFailed(QString reason) { qDebug() << "PackInstallTask::onDownloadFailed:" << QThread::currentThreadId(); jobPtr.reset(); - emitFailed(reason); + emitFailed(std::move(reason)); } void PackInstallTask::onDownloadAborted() @@ -197,26 +213,30 @@ void PackInstallTask::deleteExistingFiles() keeps.files.append(VersionKeep{ "root", "servers.dat" }); // Merge with version deletes and keeps - for (const auto& item : m_version.deletes.files) + for (const auto& item : m_version.deletes.files) { deletes.files.append(item); - for (const auto& item : m_version.deletes.folders) + } + for (const auto& item : m_version.deletes.folders) { deletes.folders.append(item); - for (const auto& item : m_version.keeps.files) + } + for (const auto& item : m_version.keeps.files) { keeps.files.append(item); - for (const auto& item : m_version.keeps.folders) + } + for (const auto& item : m_version.keeps.folders) { keeps.folders.append(item); + } auto getPathForBase = [this](const QString& base) { auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft"); if (base == "root") { return minecraftPath; - } else if (base == "config") { - return FS::PathCombine(minecraftPath, "config"); - } else { - qWarning() << "Unrecognised base path" << base; - return minecraftPath; } + if (base == "config") { + return FS::PathCombine(minecraftPath, "config"); + } + qWarning() << "Unrecognised base path" << base; + return minecraftPath; }; auto convertToSystemPath = [](const QString& path) { @@ -226,24 +246,22 @@ void PackInstallTask::deleteExistingFiles() }; auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) { - for (const auto& item : keeps.files) { - auto basePath = getPathForBase(item.base); - auto targetPath = convertToSystemPath(item.target); - auto path = FS::PathCombine(basePath, targetPath); - - if (fullPath == path) { - return true; - } + if (std::ranges::any_of(keeps.files, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto path = FS::PathCombine(basePath, targetPath); + return fullPath == path; + })) { + return true; } - for (const auto& item : keeps.folders) { - auto basePath = getPathForBase(item.base); - auto targetPath = convertToSystemPath(item.target); - auto path = FS::PathCombine(basePath, targetPath); - - if (fullPath.startsWith(path)) { - return true; - } + if (std::ranges::any_of(keeps.folders, [&fullPath, &getPathForBase, &convertToSystemPath](const auto& item) { + auto basePath = getPathForBase(item.base); + auto targetPath = convertToSystemPath(item.target); + auto path = FS::PathCombine(basePath, targetPath); + return fullPath.startsWith(path); + })) { + return true; } return false; @@ -257,8 +275,9 @@ void PackInstallTask::deleteExistingFiles() auto targetPath = convertToSystemPath(item.target); auto fullPath = FS::PathCombine(basePath, targetPath); - if (shouldKeep(fullPath)) + if (shouldKeep(fullPath)) { continue; + } filesToDelete.insert(fullPath); } @@ -272,8 +291,9 @@ void PackInstallTask::deleteExistingFiles() while (it.hasNext()) { auto path = it.next(); - if (shouldKeep(path)) + if (shouldKeep(path)) { continue; + } filesToDelete.insert(path); } @@ -285,7 +305,7 @@ void PackInstallTask::deleteExistingFiles() } } -QString PackInstallTask::getDirForModType(ModType type, QString raw) +QString PackInstallTask::getDirForModType(ModType type, const QString& raw) { switch (type) { // Mod types that can either be ignored at this stage, or ignored @@ -333,7 +353,7 @@ QString PackInstallTask::getDirForModType(ModType type, QString raw) return Q_NULLPTR; } -QString PackInstallTask::getVersionForLoader(QString uid) +QString PackInstallTask::getVersionForLoader(const QString& uid) { if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { auto vlist = APPLICATION->metadataIndex()->get(uid); @@ -354,16 +374,19 @@ QString PackInstallTask::getVersionForLoader(QString uid) // filtering for those loaders. if (m_version.loader.type != "fabric") { auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; }); - if (iter == reqs.end()) + if (iter == reqs.end()) { continue; - if (iter->equalsVersion != m_version.minecraft) + } + if (iter->equalsVersion != m_version.minecraft) { continue; + } } if (m_version.loader.recommended) { // first recommended build we find, we use. - if (!version->isRecommended()) + if (!version->isRecommended()) { continue; + } } return version->descriptor(); @@ -371,7 +394,8 @@ QString PackInstallTask::getVersionForLoader(QString uid) emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); return Q_NULLPTR; - } else if (m_version.loader.choose) { + } + if (m_version.loader.choose) { // Fabric Loader doesn't depend on a given Minecraft version. if (m_version.loader.type == "fabric") { return m_support->chooseVersion(vlist, Q_NULLPTR); @@ -415,7 +439,8 @@ QString PackInstallTask::detectLibrary(const VersionLibrary& library) if (name == QString("guava")) { return "com.google.guava:guava:" + version; - } else if (name == QString("commons-lang3")) { + } + if (name == QString("commons-lang3")) { return "org.apache.commons:commons-lang3:" + version; } } @@ -423,7 +448,7 @@ QString PackInstallTask::detectLibrary(const VersionLibrary& library) return "org.multimc.atlauncher:" + library.md5 + ":1"; } -bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr profile) +bool PackInstallTask::createLibrariesComponent(const QString& instanceRoot, PackProfile* profile) { if (m_version.libraries.isEmpty()) { return true; @@ -447,20 +472,19 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared } } - auto uuid = QUuid::createUuid(); - auto id = uuid.toString().remove('{').remove('}'); - auto target_id = "org.multimc.atlauncher." + id; + auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto targetId = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } - auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + auto patchFileName = FS::PathCombine(patchDir, targetId + ".json"); auto f = std::make_shared(); f->name = m_pack_name + " " + m_version_name + " (libraries)"; - const static QMap liteLoaderMap = { + const static QMap s_liteLoaderMap = { { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, @@ -480,8 +504,8 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared for (const auto& lib : m_version.libraries) { // If the library is LiteLoader, we need to ignore it and handle it separately. - if (liteLoaderMap.contains(lib.md5)) { - auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); + if (s_liteLoaderMap.contains(lib.md5)) { + auto ver = getComponentVersion("com.mumfrey.liteloader", s_liteLoaderMap.value(lib.md5)); if (ver) { componentsToInstall.insert("com.mumfrey.liteloader", ver); continue; @@ -498,8 +522,9 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); } } - if (libExempt) + if (libExempt) { continue; + } auto library = std::make_shared(); library->setRawName(libName); @@ -532,11 +557,11 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); + profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) }); return true; } -bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) +bool PackInstallTask::createPackComponent(const QString& instanceRoot, PackProfile* profile) { if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { return true; @@ -566,15 +591,14 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< return true; } - auto uuid = QUuid::createUuid(); - auto id = uuid.toString().remove('{').remove('}'); - auto target_id = "org.multimc.atlauncher." + id; + auto id = QUuid::createUuid().toString(QUuid::WithoutBraces); + auto targetId = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } - auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + auto patchFileName = FS::PathCombine(patchDir, targetId + ".json"); QStringList mainClasses; QStringList tweakers; @@ -601,8 +625,9 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< for (auto arg : args) { if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") { auto tweakClass = arg.remove("--tweakClass="); - if (tweakers.contains(tweakClass)) + if (tweakers.contains(tweakClass)) { continue; + } f->addTweakers.append(tweakClass); } @@ -615,13 +640,13 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { - qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + qCritical() << "Error opening" << file.fileName() << "for writing:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); + profile->appendComponent(ComponentPtr{ new Component(profile, targetId, f) }); return true; } @@ -651,7 +676,7 @@ void PackInstallTask::installConfigs() connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { abortable = false; jobPtr.reset(); - emitFailed(reason); + emitFailed(std::move(reason)); }); connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { abortable = true; @@ -708,15 +733,17 @@ void PackInstallTask::downloadMods() jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); - QList blocked_mods; + QList blockedMods; for (const auto& mod : m_version.mods) { // skip non-client mods - if (!mod.client) + if (!mod.client) { continue; + } // skip optional mods that were not selected - if (mod.optional && !selectedMods.contains(mod.name)) + if (mod.optional && !selectedMods.contains(mod.name)) { continue; + } QString url; switch (mod.download) { @@ -724,7 +751,7 @@ void PackInstallTask::downloadMods() url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; case DownloadType::Browser: { - blocked_mods.append(mod); + blockedMods.append(mod); continue; } case DownloadType::Direct: @@ -760,8 +787,9 @@ void PackInstallTask::downloadMods() jobPtr->addNetAction(dl); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); - if (relpath == Q_NULLPTR) + if (relpath == Q_NULLPTR) { continue; + } auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); @@ -795,49 +823,51 @@ void PackInstallTask::downloadMods() modsToCopy[entry->getFullPath()] = path; } } - if (!blocked_mods.isEmpty()) { + if (!blockedMods.isEmpty()) { QList mods; - for (auto mod : blocked_mods) { - BlockedMod blocked_mod; - blocked_mod.name = mod.file; - blocked_mod.websiteUrl = mod.url; - blocked_mod.hash = mod.md5; - blocked_mod.matched = false; - blocked_mod.localPath = ""; + for (const auto& mod : blockedMods) { + BlockedMod blockedMod; + blockedMod.name = mod.file; + blockedMod.websiteUrl = mod.url; + blockedMod.hash = mod.md5; + blockedMod.matched = false; + blockedMod.localPath = ""; - mods.append(blocked_mod); + mods.append(blockedMod); } qWarning() << "Blocked mods found, displaying mod list"; - BlockedModsDialog message_dialog(nullptr, tr("Blocked mods found"), - tr("The following files are not available for download in third party launchers.
" - "You will need to manually download them and add them to the instance."), - mods, "md5"); + BlockedModsDialog messageDialog(nullptr, tr("Blocked mods found"), + tr("The following files are not available for download in third party launchers.
" + "You will need to manually download them and add them to the instance."), + mods, "md5"); - message_dialog.setModal(true); + messageDialog.setModal(true); - if (message_dialog.exec()) { + if (messageDialog.exec() != 0) { qDebug() << "Post dialog blocked mods list:" << mods; - for (auto blocked : mods) { + for (const auto& blocked : mods) { if (!blocked.matched) { qDebug() << blocked.name << "was not matched to a local file, skipping copy"; continue; } - auto modIter = std::find_if(blocked_mods.begin(), blocked_mods.end(), - [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); - if (modIter == blocked_mods.end()) + auto modIter = + std::ranges::find_if(blockedMods, [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); + if (modIter == blockedMods.end()) { continue; - auto mod = *modIter; + } + const auto& mod = *modIter; if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { modsToExtract.insert(blocked.localPath, mod); } else if (mod.type == ModType::Decomp) { modsToDecomp.insert(blocked.localPath, mod); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); - if (relpath == Q_NULLPTR) + if (relpath == Q_NULLPTR) { continue; + } auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); @@ -915,8 +945,8 @@ bool PackInstallTask::extractMods(const QMap& toExtract, setStatus(tr("Extracting mods...")); for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { - auto& modPath = iter.key(); - auto& mod = iter.value(); + const auto& modPath = iter.key(); + const auto& mod = iter.value(); QString extractToDir; if (mod.type == ModType::Extract) { @@ -935,6 +965,10 @@ bool PackInstallTask::extractMods(const QMap& toExtract, folderToExtract = mod.extractFolder; static const QRegularExpression s_regex("^/"); folderToExtract.remove(s_regex); + if (isPathTraversal(extractToPath, folderToExtract)) { + qWarning() << "Blocked path traversal in" << mod.extractFolder; + return false; + } } qDebug() << "Extracting " + mod.file + " to " + extractToDir; @@ -945,13 +979,18 @@ bool PackInstallTask::extractMods(const QMap& toExtract, } for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { - auto& modPath = iter.key(); - auto& mod = iter.value(); + const auto& modPath = iter.key(); + const auto& mod = iter.value(); auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); QDir extractDir(m_stagingPath); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + if (isPathTraversal(extractToPath, mod.decompFile)) { + qWarning() << "Blocked path traversal in decompFile" << mod.decompFile; + return false; + } + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { qWarning() << "Failed to extract" << mod.decompFile; @@ -960,8 +999,8 @@ bool PackInstallTask::extractMods(const QMap& toExtract, } for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { - auto& from = iter.key(); - auto& to = iter.value(); + const auto& from = iter.key(); + const auto& to = iter.value(); // If the file already exists, assume the mod is the correct copy - and remove // the copy from the Configs.zip @@ -988,74 +1027,71 @@ void PackInstallTask::install() setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(instanceConfigPath); - instanceSettings->suspendSave(); + MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); + { + SettingsObject::Lock lock(instance.settings()); + auto* components = instance.getPackProfile(); + components->buildingFromScratch(); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - auto components = instance.getPackProfile(); - components->buildingFromScratch(); - - // Use a component to add libraries BEFORE Minecraft - if (!createLibrariesComponent(instance.instanceRoot(), components)) { - emitFailed(tr("Failed to create libraries component")); - return; - } - - // Minecraft - components->setComponentVersion("net.minecraft", m_version.minecraft, true); - - // Loader - if (m_version.loader.type == QString("forge")) { - auto version = getVersionForLoader("net.minecraftforge"); - if (version == Q_NULLPTR) + // Use a component to add libraries BEFORE Minecraft + if (!createLibrariesComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create libraries component")); return; + } - components->setComponentVersion("net.minecraftforge", version); - } else if (m_version.loader.type == QString("neoforge")) { - auto version = getVersionForLoader("net.neoforged"); - if (version == Q_NULLPTR) + // Minecraft + components->setComponentVersion("net.minecraft", m_version.minecraft, true); + + // Loader + if (m_version.loader.type == QString("forge")) { + auto version = getVersionForLoader("net.minecraftforge"); + if (version == Q_NULLPTR) { + return; + } + + components->setComponentVersion("net.minecraftforge", version); + } else if (m_version.loader.type == QString("neoforge")) { + auto version = getVersionForLoader("net.neoforged"); + if (version == Q_NULLPTR) { + return; + } + + components->setComponentVersion("net.neoforged", version); + } else if (m_version.loader.type == QString("fabric")) { + auto version = getVersionForLoader("net.fabricmc.fabric-loader"); + if (version == Q_NULLPTR) { + return; + } + + components->setComponentVersion("net.fabricmc.fabric-loader", version); + } else if (m_version.loader.type != QString()) { + emitFailed(tr("Unknown loader type: ") + m_version.loader.type); return; + } - components->setComponentVersion("net.neoforged", version); - } else if (m_version.loader.type == QString("fabric")) { - auto version = getVersionForLoader("net.fabricmc.fabric-loader"); - if (version == Q_NULLPTR) + for (const auto& componentUid : componentsToInstall.keys()) { + auto version = componentsToInstall.value(componentUid); + components->setComponentVersion(componentUid, version->version()); + } + + components->installJarMods(jarmods); + + // Use a component to fill in the rest of the data + // todo: use more detection + if (!createPackComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create pack component")); return; + } - components->setComponentVersion("net.fabricmc.fabric-loader", version); - } else if (m_version.loader.type != QString()) { - emitFailed(tr("Unknown loader type: ") + m_version.loader.type); - return; + components->saveNow(); + + instance.setName(name()); + instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); + + jarmods.clear(); } - - for (const auto& componentUid : componentsToInstall.keys()) { - auto version = componentsToInstall.value(componentUid); - components->setComponentVersion(componentUid, version->version()); - } - - components->installJarMods(jarmods); - - // Use a component to fill in the rest of the data - // todo: use more detection - if (!createPackComponent(instance.instanceRoot(), components)) { - emitFailed(tr("Failed to create pack component")); - return; - } - - components->saveNow(); - - instance.setName(name()); - instance.setIconKey(m_instIcon); - instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); - instanceSettings->resumeSave(); - - jarmods.clear(); emitSucceeded(); } -static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) -{ - return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); -} - } // namespace ATLauncher diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 8024286e8..e8df132be 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -44,14 +44,13 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/NetJob.h" -#include "settings/INISettingsObject.h" -#include +#include #include namespace ATLauncher { -enum class InstallMode { +enum class InstallMode : std::uint8_t { Install, Reinstall, Update, @@ -86,16 +85,16 @@ class PackInstallTask : public InstanceTask { QString packName, QString version, InstallMode installMode = InstallMode::Install); - virtual ~PackInstallTask() { delete m_support; } + ~PackInstallTask() override { delete m_support; } bool canAbort() const override { return true; } bool abort() override; protected: - virtual void executeTask() override; + void executeTask() override; private slots: - void onDownloadSucceeded(); + void onDownloadSucceeded(QByteArray* responsePtr); void onDownloadFailed(QString reason); void onDownloadAborted(); @@ -103,12 +102,12 @@ class PackInstallTask : public InstanceTask { void onModsExtracted(); private: - QString getDirForModType(ModType type, QString raw); - QString getVersionForLoader(QString uid); - QString detectLibrary(const VersionLibrary& library); + QString getDirForModType(ModType type, const QString& raw); + QString getVersionForLoader(const QString& uid); + static QString detectLibrary(const VersionLibrary& library); - bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); - bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + bool createLibrariesComponent(const QString& instanceRoot, PackProfile* profile); + bool createPackComponent(const QString& instanceRoot, PackProfile* profile); void deleteExistingFiles(); void installConfigs(); @@ -125,7 +124,6 @@ class PackInstallTask : public InstanceTask { bool abortable = false; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); InstallMode m_install_mode; QString m_pack_name; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 3fe71dbc4..9cd85aef7 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -29,6 +29,8 @@ #include "net/NetJob.h" #include "tasks/Task.h" +#include "Application.h" + static const FlameAPI flameAPI; static ModrinthAPI modrinthAPI; @@ -51,19 +53,19 @@ void Flame::FileResolvingTask::executeTask() } setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); - m_result.reset(new QByteArray()); QStringList fileIds; for (auto file : m_manifest.files) { fileIds.push_back(QString::number(file.fileId)); } - m_task = flameAPI.getFiles(fileIds, m_result); + auto [task, response] = flameAPI.getFiles(fileIds); + m_task = task; auto step_progress = std::make_shared(); - connect(m_task.get(), &Task::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::succeeded, this, [this, response, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); - netJobFinished(); + netJobFinished(response); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; @@ -108,7 +110,7 @@ ModPlatform::ResourceType getResourceType(int classId) } } -void Flame::FileResolvingTask::netJobFinished() +void Flame::FileResolvingTask::netJobFinished(QByteArray* response) { setProgress(1, 3); // job to check modrinth for blocked projects @@ -116,7 +118,7 @@ void Flame::FileResolvingTask::netJobFinished() QJsonArray array; try { - doc = Json::requireDocument(*m_result); + doc = Json::requireDocument(*response); array = Json::requireArray(doc.object()["data"]); } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; @@ -153,56 +155,53 @@ void Flame::FileResolvingTask::netJobFinished() getFlameProjects(); return; } - m_result.reset(new QByteArray()); - m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); + auto [modrinthTask, modrinthResponse] = modrinthAPI.currentVersions(hashes, "sha1"); + m_task = modrinthTask; (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); - connect(m_task.get(), &Task::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::succeeded, this, [this, modrinthResponse, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*modrinthResponse, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *m_result; + qWarning() << *modrinthResponse; getFlameProjects(); return; - } + } + if (APPLICATION->settings()->get("FallbackMRBlockedMods").toBool()){ + try { + auto entries = Json::requireObject(doc); + for (auto& out : m_manifest.files) { + auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { + try { + auto entry = Json::requireObject(entries, out.version.hash); - try { - auto entries = Json::requireObject(doc); - for (auto& out : m_manifest.files) { - auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); - if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { - try { - auto entry = Json::requireObject(entries, out.version.hash); + auto file = Modrinth::loadIndexedPackVersion(entry); - auto file = Modrinth::loadIndexedPackVersion(entry); - - // If there's more than one mod loader for this version, we can't know for sure - // which file is relative to each loader, so it's best to not use any one and - // let the user download it manually. - if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { out.version.downloadUrl = file.downloadUrl; qDebug() << "Found alternative on modrinth" << out.version.fileName; + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; } - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - qDebug() << entries; } } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; } - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - qDebug() << doc; } getFlameProjects(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); + getFlameProjects(); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { @@ -214,29 +213,28 @@ void Flame::FileResolvingTask::netJobFinished() step_progress->status = status; stepProgress(*step_progress); }); - m_task->start(); } void Flame::FileResolvingTask::getFlameProjects() { setProgress(2, 3); - m_result.reset(new QByteArray()); QStringList addonIds; for (auto file : m_manifest.files) { addonIds.push_back(QString::number(file.projectId)); } - m_task = flameAPI.getProjects(addonIds, m_result); + auto [task, response] = flameAPI.getProjects(addonIds); + m_task = task; auto step_progress = std::make_shared(); - connect(m_task.get(), &Task::succeeded, this, [this, step_progress] { + connect(m_task.get(), &Task::succeeded, this, [this, response, step_progress] { QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*m_result, &parse_error); + auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *m_result; + qWarning() << *response; return; } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index 3fe8dfb1a..21fa53d2d 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -36,14 +36,13 @@ class FileResolvingTask : public Task { virtual void executeTask() override; protected slots: - void netJobFinished(); + void netJobFinished(QByteArray* response); private: void getFlameProjects(); private: /* data */ Flame::Manifest m_manifest; - std::shared_ptr m_result; Task::Ptr m_task; }; } // namespace Flame diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 34b8d0a03..b9b5c2207 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -15,7 +15,7 @@ #include "net/ApiUpload.h" #include "net/NetJob.h" -Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) +std::pair FlameAPI::matchFingerprints(const QList& fingerprints) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); @@ -29,10 +29,10 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shar QJsonDocument body(body_obj); auto body_raw = body.toJson(); + auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), body_raw); + netJob->addNetAction(action); - netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw)); - - return netJob; + return { netJob, response }; } QString FlameAPI::getModFileChangelog(int modId, int fileId) @@ -41,11 +41,10 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId) QString changelog; auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); - auto response = std::make_shared(); - netJob->addNetAction(Net::ApiDownload::makeByteArray( + auto [action, response] = Net::ApiDownload::makeByteArray( QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") - .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), - response)); + .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId)))); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; @@ -76,9 +75,9 @@ QString FlameAPI::getModDescription(int modId) QString description; auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); - auto response = std::make_shared(); - netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)), response)); + auto [action, response] = + Net::ApiDownload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId))); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -103,7 +102,7 @@ QString FlameAPI::getModDescription(int modId) return description; } -Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +std::pair FlameAPI::getProjects(QStringList addonIds) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); @@ -117,15 +116,15 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw)); + auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), body_raw); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); - return netJob; + return { netJob, response }; } -Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const +std::pair FlameAPI::getFiles(const QStringList& fileIds) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); @@ -140,22 +139,24 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw)); + auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), body_raw); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); - return netJob; + return { netJob, response }; } -Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const +std::pair FlameAPI::getFile(const QString& addonId, const QString& fileId) const { auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); - netJob->addNetAction( - Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)), response)); + auto [action, response] = + Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId))); + netJob->addNetAction(action); QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); - return netJob; + return { netJob, response }; } QList FlameAPI::getSortingMethods() const @@ -171,25 +172,26 @@ QList FlameAPI::getSortingMethods() const { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } -Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) +std::pair FlameAPI::getCategories(ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray( - QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); + auto [action, response] = Net::ApiDownload::makeByteArray( + QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type)))); + netJob->addNetAction(action); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); - return netJob; + return { netJob, response }; } -Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +std::pair FlameAPI::getModCategories() { - return getCategories(response, ModPlatform::ResourceType::Mod); + return getCategories(ModPlatform::ResourceType::Mod); } -QList FlameAPI::loadModCategories(std::shared_ptr response) +QList FlameAPI::loadModCategories(const QByteArray& response) { QList categories; QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 8bcb3ff46..e77d53a4b 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -5,7 +5,7 @@ #pragma once #include -#include +#include #include "BuildConfig.h" #include "Json.h" #include "Version.h" @@ -23,14 +23,14 @@ class FlameAPI : public ResourceAPI { ModPlatform::ModLoaderTypes fallback, bool checkLoaders); - Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; - Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); - Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; - Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; + std::pair getProjects(QStringList addonIds) const override; + std::pair matchFingerprints(const QList& fingerprints); + std::pair getFiles(const QStringList& fileIds) const; + std::pair getFile(const QString& addonId, const QString& fileId) const; - static Task::Ptr getCategories(std::shared_ptr response, ModPlatform::ResourceType type); - static Task::Ptr getModCategories(std::shared_ptr response); - static QList loadModCategories(std::shared_ptr response); + static std::pair getCategories(ModPlatform::ResourceType type); + static std::pair getModCategories(); + static QList loadModCategories(const QByteArray& response); QList getSortingMethods() const override; @@ -79,6 +79,7 @@ class FlameAPI : public ResourceAPI { case ModPlatform::LegacyFabric: case ModPlatform::Ornithe: case ModPlatform::Rift: + case ModPlatform::None: break; // not supported } return 0; @@ -98,7 +99,7 @@ class FlameAPI : public ResourceAPI { static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } public: - std::optional getSearchURL(SearchArgs const& args) const override + std::optional getSearchURL(const SearchArgs& args) const override { QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); @@ -111,7 +112,7 @@ class FlameAPI : public ResourceAPI { get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) { ModPlatform::ModLoaderTypes loaders = args.loaders.value(); - loaders &= ~ModPlatform::ModLoaderType::DataPack; + loaders &= ~static_cast(ModPlatform::ModLoaderType::DataPack); if (loaders != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); } @@ -124,7 +125,7 @@ class FlameAPI : public ResourceAPI { return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } - std::optional getVersionsURL(VersionSearchArgs const& args) const override + std::optional getVersionsURL(const VersionSearchArgs& args) const override { auto addonId = args.pack->addonId.toString(); QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); @@ -149,10 +150,10 @@ class FlameAPI : public ResourceAPI { return arr; } // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. - auto const& mc_versions = arr.mcVersion; + const auto& mc_versions = arr.mcVersion; if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), - [](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); })) { + [](const auto& mc_version) { return Version(mc_version) <= Version("1.6"); })) { return arr; } return {}; @@ -160,8 +161,8 @@ class FlameAPI : public ResourceAPI { void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); } private: - std::optional getInfoURL(QString const& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } - std::optional getDependencyURL(DependencySearchArgs const& args) const override + std::optional getInfoURL(const QString& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } + std::optional getDependencyURL(const DependencySearchArgs& args) const override { auto addonId = args.dependency.addonId.toString(); auto url = diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index d6d61e213..577f9967a 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -18,8 +18,6 @@ #include "net/NetJob.h" #include "tasks/Task.h" -static FlameAPI api; - bool FlameCheckUpdate::abort() { bool result = false; @@ -39,7 +37,7 @@ void FlameCheckUpdate::executeTask() { setStatus(tr("Preparing resources for CurseForge...")); - auto netJob = new NetJob("Get latest versions", APPLICATION->network()); + auto* netJob = new NetJob("Get latest versions", APPLICATION->network()); connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); @@ -48,12 +46,12 @@ void FlameCheckUpdate::executeTask() for (auto* resource : m_resources) { auto project = std::make_shared(); project->addonId = resource->metadata()->project_id.toString(); - auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions }); - if (!versionsUrlOptional.has_value()) + auto versionsUrlOptional = FlameAPI().getVersionsURL({ .pack = project, .mcVersions = m_gameVersions }); + if (!versionsUrlOptional.has_value()) { continue; + } - auto response = std::make_shared(); - auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response); + auto [task, response] = Net::ApiDownload::makeByteArray(versionsUrlOptional.value()); connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); }); netJob->addNetAction(task); @@ -62,13 +60,13 @@ void FlameCheckUpdate::executeTask() m_task->start(); } -void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr response) +void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, QByteArray* response) { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from latest mod version at" << parse_error.offset - << "reason:" << parse_error.errorString(); + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from latest mod version at" << parseError.offset + << "reason:" << parseError.errorString(); qWarning() << *response; return; } @@ -89,100 +87,104 @@ void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ qCritical() << e.what(); qDebug() << doc; } - auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); + auto latestVer = FlameAPI().getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); - if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { + if (!latestVer.has_value() || !latestVer->addonId.isValid()) { QString reason; - if (dynamic_cast(resource) != nullptr) + if (dynamic_cast(resource) != nullptr) { reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); - else + } else { reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); + } emit checkFailed(resource, reason); return; } - if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { - m_blocked[resource] = latest_ver->fileId.toString(); + if (latestVer->downloadUrl.isEmpty() && latestVer->fileId != resource->metadata()->file_id) { + m_blocked[resource] = latestVer->fileId.toString(); return; } - if (!latest_ver->hash.isEmpty() && - (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { - auto old_version = resource->metadata()->version_number; - if (old_version.isEmpty()) { - if (resource->status() == ResourceStatus::NOT_INSTALLED) - old_version = tr("Not installed"); - else - old_version = tr("Unknown"); + if (!latestVer->hash.isEmpty() && + (resource->metadata()->hash != latestVer->hash || resource->status() == ResourceStatus::NotInstalled)) { + auto oldVersion = resource->metadata()->version_number; + if (oldVersion.isEmpty()) { + if (resource->status() == ResourceStatus::NotInstalled) { + oldVersion = tr("Not installed"); + } else { + oldVersion = tr("Unknown"); + } } - auto download_task = makeShared(pack, latest_ver.value(), m_resourceModel); - m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, - api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), - ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled()); + auto downloadTask = makeShared(pack, latestVer.value(), m_resourceModel, true, "update"); + m_updates.emplace_back(pack->name, resource->metadata()->hash, oldVersion, latestVer->version, latestVer->version_type, + FlameAPI().getModFileChangelog(latestVer->addonId.toInt(), latestVer->fileId.toInt()), + ModPlatform::ResourceProvider::FLAME, downloadTask, resource->enabled()); } - m_deps.append(std::make_shared(pack, latest_ver.value())); + m_deps.append(std::make_shared(pack, latestVer.value())); } void FlameCheckUpdate::collectBlockedMods() { QStringList addonIds; QHash quickSearch; - for (auto const& resource : m_blocked.keys()) { + for (const auto& resource : m_blocked.keys()) { auto addonId = resource->metadata()->project_id.toString(); addonIds.append(addonId); quickSearch[addonId] = resource; } - auto response = std::make_shared(); Task::Ptr projTask; + QByteArray* response = nullptr; if (addonIds.isEmpty()) { emitSucceeded(); return; - } else if (addonIds.size() == 1) { - projTask = api.getProject(*addonIds.begin(), response); + } + if (addonIds.size() == 1) { + std::tie(projTask, response) = FlameAPI().getProject(*addonIds.begin()); } else { - projTask = api.getProjects(addonIds, response); + std::tie(projTask, response) = FlameAPI().getProjects(addonIds); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { - QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame projects task at" << parse_error.offset - << "reason:" << parse_error.errorString(); + QJsonParseError parseError{}; + auto doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame projects task at" << parseError.offset + << "reason:" << parseError.errorString(); qWarning() << *response; return; } try { QJsonArray entries; - if (addonIds.size() == 1) + if (addonIds.size() == 1) { entries = { Json::requireObject(Json::requireObject(doc), "data") }; - else + } else { entries = Json::requireArray(Json::requireObject(doc), "data"); + } for (auto entry : entries) { - auto entry_obj = Json::requireObject(entry); + auto entryObj = Json::requireObject(entry); - auto id = QString::number(Json::requireInteger(entry_obj, "id")); + auto id = QString::number(Json::requireInteger(entryObj, "id")); - auto resource = quickSearch.find(id).value(); + auto* resource = quickSearch.find(id).value(); ModPlatform::IndexedPack pack; try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); - FlameMod::loadIndexedPack(pack, entry_obj); - auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); + FlameMod::loadIndexedPack(pack, entryObj); + auto recoverUrl = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), - recover_url); + recoverUrl); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; @@ -200,4 +202,4 @@ void FlameCheckUpdate::collectBlockedMods() connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); m_task.reset(projTask); m_task->start(); -} \ No newline at end of file +} diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index eb80ce47c..c2b3c9c35 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -7,10 +7,10 @@ class FlameCheckUpdate : public CheckUpdateTask { public: FlameCheckUpdate(QList& resources, - std::list& mcVersions, + std::vector& mcVersions, QList loadersList, - std::shared_ptr resourceModel) - : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) + ResourceFolderModel* resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), resourceModel) {} public slots: @@ -19,7 +19,7 @@ class FlameCheckUpdate : public CheckUpdateTask { protected slots: void executeTask() override; private slots: - void getLatestVersionCallback(Resource* resource, std::shared_ptr response); + void getLatestVersionCallback(Resource* resource, QByteArray* response); void collectBlockedMods(); private: diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 6a3215275..534132a6e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -35,6 +35,7 @@ #include "FlameInstanceCreationTask.h" +#include "InstanceTask.h" #include "QObjectPtr.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" @@ -54,7 +55,7 @@ #include "settings/INISettingsObject.h" -#include "sys.h" +#include "SysInfo.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -62,6 +63,7 @@ #include #include +#include "HardwareInfo.h" #include "meta/Index.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" @@ -75,7 +77,6 @@ bool FlameCreationTask::abort() if (!canAbort()) return false; - m_abort = true; if (m_processUpdateFileInfoJob) m_processUpdateFileInfoJob->abort(); if (m_filesJob) @@ -83,7 +84,7 @@ bool FlameCreationTask::abort() if (m_modIdResolver) m_modIdResolver->abort(); - return Task::abort(); + return InstanceCreationTask::abort(); } bool FlameCreationTask::updateInstance() @@ -91,7 +92,7 @@ bool FlameCreationTask::updateInstance() auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - InstancePtr inst; + BaseInstance* inst; if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { inst = instance_list->getInstanceById(original_id); Q_ASSERT(inst); @@ -171,10 +172,7 @@ bool FlameCreationTask::updateInstance() // FIXME: We may want to do something about disabled mods. auto old_overrides = Override::readOverrides("overrides", old_index_folder); for (const auto& entry : old_overrides) { - if (entry.isEmpty()) - continue; - qDebug() << "Scheduling" << entry << "for removal"; - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + scheduleToDelete(m_parent, old_minecraft_dir, entry); } // Remove remaining old files (we need to do an API request to know which ids are which files...) @@ -184,8 +182,7 @@ bool FlameCreationTask::updateInstance() fileIds.append(QString::number(file.fileId)); } - auto raw_response = std::make_shared(); - auto job = api.getFiles(fileIds, raw_response); + auto [job, raw_response] = api.getFiles(fileIds); QEventLoop loop; @@ -226,13 +223,7 @@ bool FlameCreationTask::updateInstance() continue; QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); - qDebug() << "Scheduling" << relative_path << "for removal"; - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); - if (relative_path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9))); - } else { - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled")); - } + scheduleToDelete(m_parent, old_minecraft_dir, relative_path, true); } }); connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files:" << reason; }); @@ -316,7 +307,7 @@ QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, return loaderVersion; } -bool FlameCreationTask::createInstance() +std::unique_ptr FlameCreationTask::createInstance() { QEventLoop loop; @@ -334,7 +325,7 @@ bool FlameCreationTask::createInstance() } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); - return false; + return nullptr; } if (!m_pack.overrides.isEmpty()) { @@ -346,7 +337,7 @@ bool FlameCreationTask::createInstance() QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); - return false; + return nullptr; } } else { logWarning( @@ -386,8 +377,8 @@ bool FlameCreationTask::createInstance() } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(configPath); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto instanceSettings = std::make_unique(configPath); + auto instance = std::make_unique(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto mcVersion = m_pack.minecraft.version; // Hack to correct some 'special sauce'... @@ -397,33 +388,33 @@ bool FlameCreationTask::createInstance() logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } - auto components = instance.getPackProfile(); + auto components = instance->getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", mcVersion, true); if (!loaderType.isEmpty()) { auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion); if (version.isEmpty()) - return false; + return nullptr; components->setComponentVersion(loaderUid, version); } if (m_instIcon != "default") { - instance.setIconKey(m_instIcon); + instance->setIconKey(m_instIcon); } else { if (m_pack.name.contains("Direwolf20")) { - instance.setIconKey("steve"); + instance->setIconKey("steve"); } else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) { - instance.setIconKey("ftb_logo"); + instance->setIconKey("ftb_logo"); } else { - instance.setIconKey("flame"); + instance->setIconKey("flame"); } } int recommendedRAM = m_pack.minecraft.recommendedRAM; // only set memory if this is a fresh instance - if (m_instance == nullptr && recommendedRAM > 0) { - const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte; + if (!m_instance && recommendedRAM > 0) { + const uint64_t sysMiB = HardwareInfo::totalRamMiB(); const uint64_t max = sysMiB * 0.9; if (static_cast(recommendedRAM) > max) { @@ -433,8 +424,8 @@ bool FlameCreationTask::createInstance() recommendedRAM = max; } - instance.settings()->set("OverrideMemory", true); - instance.settings()->set("MaxMemAlloc", recommendedRAM); + instance->settings()->set("OverrideMemory", true); + instance->settings()->set("MaxMemAlloc", recommendedRAM); } QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); @@ -448,7 +439,7 @@ bool FlameCreationTask::createInstance() qDebug() << info.fileName(); jarMods.push_back(info.absoluteFilePath()); } - auto profile = instance.getPackProfile(); + auto profile = instance->getPackProfile(); profile->installJarMods(jarMods); // nuke the original files FS::deletePath(jarmodsPath); @@ -456,11 +447,11 @@ bool FlameCreationTask::createInstance() // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managedId.isEmpty()) - instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); + instance->setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); else - instance.setManagedPack("flame", "", name(), "", ""); + instance->setManagedPack("flame", "", name(), "", ""); - instance.setName(name()); + instance->setName(name()); m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack)); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); @@ -485,10 +476,13 @@ bool FlameCreationTask::createInstance() setAbortable(false); auto inst = m_instance.value(); - inst->copyManagedPack(instance); + inst->copyManagedPack(*instance); } - return did_succeed; + if (did_succeed) { + return instance; + } + return nullptr; } void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index e41ce742e..221ceaf22 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -52,7 +52,7 @@ class FlameCreationTask final : public InstanceCreationTask { public: FlameCreationTask(const QString& staging_path, - SettingsObjectPtr global_settings, + SettingsObject* global_settings, QWidget* parent, QString id, QString version_id, @@ -68,7 +68,7 @@ class FlameCreationTask final : public InstanceCreationTask { bool abort() override; bool updateInstance() override; - bool createInstance() override; + std::unique_ptr createInstance() override; private slots: void idResolverSucceeded(QEventLoop&); @@ -91,7 +91,7 @@ class FlameCreationTask final : public InstanceCreationTask { QList> m_otherResources; - std::optional m_instance; + std::optional m_instance; QStringList m_selectedOptionalMods; }; diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 621443985..8be4fe94a 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -67,8 +67,8 @@ void FlamePackExportTask::collectFiles() setAbortable(false); QCoreApplication::processEvents(); - files.clear(); - if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) { + m_files.clear(); + if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &m_files, m_options.filter)) { emitFailed(tr("Could not search for files")); return; } @@ -77,7 +77,7 @@ void FlamePackExportTask::collectFiles() resolvedFiles.clear(); m_options.instance->loaderModList()->update(); - connect(m_options.instance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); + connect(m_options.instance->loaderModList(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); } void FlamePackExportTask::collectHashes() @@ -88,7 +88,7 @@ void FlamePackExportTask::collectHashes() auto allMods = m_options.instance->loaderModList()->allMods(); ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); - for (const QFileInfo& file : files) { + for (const QFileInfo& file : m_files) { const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { @@ -167,14 +167,14 @@ void FlamePackExportTask::makeApiRequest() setStatus(tr("Finding versions for hashes...")); setProgress(2, 5); - auto response = std::make_shared(); QList fingerprints; for (auto& murmur : pendingHashes.keys()) { fingerprints.push_back(murmur.toUInt()); } - task.reset(api.matchFingerprints(fingerprints, response)); + auto [matchTask, response] = api.matchFingerprints(fingerprints); + task = matchTask; connect(task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parseError{}; @@ -245,16 +245,16 @@ void FlamePackExportTask::getProjectsInfo() } } - auto response = std::make_shared(); Task::Ptr projTask; + QByteArray* response; if (addonIds.isEmpty()) { buildZip(); return; } else if (addonIds.size() == 1) { - projTask = api.getProject(*addonIds.begin(), response); + std::tie(projTask, response) = api.getProject(*addonIds.begin()); } else { - projTask = api.getProjects(addonIds, response); + std::tie(projTask, response) = api.getProjects(addonIds); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] { @@ -319,7 +319,7 @@ void FlamePackExportTask::buildZip() setStatus(tr("Adding files...")); setProgress(4, 5); - auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true); + auto zipTask = makeShared(m_options.output, m_gameRoot, m_files, "overrides/", true); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index e3d4c74a7..f6a90241d 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -29,7 +29,7 @@ struct FlamePackExportOptions { QString version; QString author; bool optionalFiles; - MinecraftInstancePtr instance; + MinecraftInstance* instance; QString output; MMCZip::FilterFileFunction filter; int recommendedRAM; @@ -72,7 +72,7 @@ class FlamePackExportTask : public Task { FlameAPI api; - QFileInfoList files; + QFileInfoList m_files; QMap pendingHashes{}; QMap resolvedFiles{}; Task::Ptr task; diff --git a/launcher/modplatform/ftb/FTBPackInstallTask.cpp b/launcher/modplatform/ftb/FTBPackInstallTask.cpp new file mode 100644 index 000000000..6081807cf --- /dev/null +++ b/launcher/modplatform/ftb/FTBPackInstallTask.cpp @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 flowln + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2021 Petr Mrazek + * + * 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 "FTBPackInstallTask.h" + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "modplatform/flame/FileResolvingTask.h" +#include "modplatform/flame/PackManifest.h" +#include "net/ChecksumValidator.h" +#include "settings/INISettingsObject.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "ui/dialogs/BlockedModsDialog.h" + +namespace FTB { + +PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent) + : m_pack(std::move(pack)), m_versionName(std::move(version)), m_parent(parent) +{} + +bool PackInstallTask::abort() +{ + if (!canAbort()) + return false; + + bool aborted = true; + + if (m_net_job) + aborted &= m_net_job->abort(); + if (m_modIdResolverTask) + aborted &= m_modIdResolverTask->abort(); + + return aborted ? InstanceTask::abort() : false; +} + +void PackInstallTask::executeTask() +{ + setStatus(tr("Getting the manifest...")); + setAbortable(false); + + // Find pack version + auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(), + [this](const FTB::VersionInfo& a) { return a.name == m_versionName; }); + + if (version_it == m_pack.versions.constEnd()) { + emitFailed(tr("Failed to find pack version %1").arg(m_versionName)); + return; + } + + auto version = *version_it; + + auto netJob = makeShared("FTB::VersionFetch", APPLICATION->network()); + + auto searchUrl = QString(BuildConfig.FTB_API_BASE_URL + "/modpack/%1/%2").arg(m_pack.id).arg(version.id); + + auto [action, response] = Net::Download::makeByteArray(QUrl(searchUrl)); + netJob->addNetAction(action); + + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, response] { onManifestDownloadSucceeded(response); }); + QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed); + QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::abort); + QObject::connect(netJob.get(), &NetJob::progress, this, &PackInstallTask::setProgress); + + m_net_job = netJob; + + setAbortable(true); + netJob->start(); +} + +void PackInstallTask::onManifestDownloadSucceeded(QByteArray* responsePtr) +{ + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by m_net_job.reset() + QByteArray response = std::move(*responsePtr); + m_net_job.reset(); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + FTB::Version version; + try { + auto obj = Json::requireObject(doc); + FTB::loadVersion(version, obj); + } catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + + m_version = version; + + resolveMods(); +} + +void PackInstallTask::resolveMods() +{ + setStatus(tr("Resolving mods...")); + setAbortable(false); + setProgress(0, 100); + + m_fileIds.clear(); + + Flame::Manifest manifest; + for (const auto& file : m_version.files) { + if (!file.serverOnly && file.url.isEmpty()) { + if (file.curseforge.file_id <= 0) { + emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name)); + return; + } + + Flame::File flameFile; + flameFile.projectId = file.curseforge.project_id; + flameFile.fileId = file.curseforge.file_id; + + manifest.files.insert(flameFile.fileId, flameFile); + m_fileIds.append(flameFile.fileId); + } else { + m_fileIds.append(-1); + } + } + + m_modIdResolverTask.reset(new Flame::FileResolvingTask(manifest)); + + connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded); + connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed); + connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::aborted, this, &PackInstallTask::abort); + connect(m_modIdResolverTask.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress); + + setAbortable(true); + + m_modIdResolverTask->start(); +} + +void PackInstallTask::onResolveModsSucceeded() +{ + auto anyBlocked = false; + + Flame::Manifest results = m_modIdResolverTask->getResults(); + for (int index = 0; index < m_fileIds.size(); index++) { + const auto file_id = m_fileIds.at(index); + if (file_id < 0) + continue; + + Flame::File resultsFile = results.files[file_id]; + VersionFile& localFile = m_version.files[index]; + + // First check for blocked mods + if (resultsFile.version.downloadUrl.isEmpty()) { + BlockedMod blocked_mod; + blocked_mod.name = resultsFile.version.fileName; + blocked_mod.websiteUrl = QString("%1/download/%2").arg(resultsFile.pack.websiteUrl, QString::number(resultsFile.fileId)); + blocked_mod.hash = resultsFile.version.hash; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + blocked_mod.targetFolder = resultsFile.targetFolder; + + m_blockedMods.append(blocked_mod); + + anyBlocked = true; + } else { + localFile.url = resultsFile.version.downloadUrl; + } + } + + m_modIdResolverTask.reset(); + + if (anyBlocked) { + qDebug() << "Blocked files found, displaying file list"; + + BlockedModsDialog message_dialog(m_parent, tr("Blocked files found"), + tr("The following files are not available for download in third party launchers.
" + "You will need to manually download them and add them to the instance."), + m_blockedMods); + + message_dialog.setModal(true); + + if (message_dialog.exec() == QDialog::Accepted) { + qDebug() << "Post dialog blocked mods list: " << m_blockedMods; + createInstance(); + } else { + abort(); + } + + } else { + createInstance(); + } +} + +void PackInstallTask::createInstance() +{ + setAbortable(false); + + setStatus(tr("Creating the instance...")); + QCoreApplication::processEvents(); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_unique(instanceConfigPath); + + MinecraftInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + for (auto target : m_version.targets) { + if (target.type == "game" && target.name == "minecraft") { + components->setComponentVersion("net.minecraft", target.version, true); + break; + } + } + + for (auto target : m_version.targets) { + if (target.type != "modloader") + continue; + + if (target.name == "forge") { + components->setComponentVersion("net.minecraftforge", target.version); + } else if (target.name == "fabric") { + components->setComponentVersion("net.fabricmc.fabric-loader", target.version); + } else if (target.name == "neoforge") { + components->setComponentVersion("net.neoforged", target.version); + } else if (target.name == "quilt") { + components->setComponentVersion("org.quiltmc.quilt-loader", target.version); + } + } + + // install any jar mods + QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods")); + if (jarModsDir.exists()) { + QStringList jarMods; + + for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { + jarMods.push_back(info.absoluteFilePath()); + } + + components->installJarMods(jarMods); + } + + components->saveNow(); + + instance.setName(name()); + instance.setIconKey(m_instIcon); + instance.setManagedPack("ftb", QString::number(m_pack.id), m_pack.name, QString::number(m_version.id), m_version.name); + + instance.saveNow(); + + onCreateInstanceSucceeded(); +} + +void PackInstallTask::onCreateInstanceSucceeded() +{ + downloadPack(); +} + +void PackInstallTask::downloadPack() +{ + setStatus(tr("Downloading mods...")); + setAbortable(false); + + auto jobPtr = makeShared(tr("Mod download"), APPLICATION->network()); + for (const auto& file : m_version.files) { + if (file.serverOnly || file.url.isEmpty()) + continue; + + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path, file.name); + qDebug() << "Will try to download" << file.url << "to" << path; + + QFileInfo file_info(file.name); + + auto dl = Net::Download::makeFile(file.url, path); + if (!file.sha1.isEmpty()) { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.sha1)); + } + + jobPtr->addNetAction(dl); + } + + jobPtr->setMaxConcurrent(1); // FTB blocks multiple requests at a time + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded); + connect(jobPtr.get(), &NetJob::failed, this, &PackInstallTask::onModDownloadFailed); + connect(jobPtr.get(), &NetJob::aborted, this, &PackInstallTask::abort); + connect(jobPtr.get(), &NetJob::progress, this, &PackInstallTask::setProgress); + + m_net_job = jobPtr; + + setAbortable(true); + jobPtr->start(); +} + +void PackInstallTask::onModDownloadSucceeded() +{ + m_net_job.reset(); + if (!m_blockedMods.isEmpty()) { + copyBlockedMods(); + } + emitSucceeded(); +} + +void PackInstallTask::onManifestDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} +void PackInstallTask::onResolveModsFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} +void PackInstallTask::onCreateInstanceFailed(QString reason) +{ + emitFailed(reason); +} +void PackInstallTask::onModDownloadFailed(QString reason) +{ + m_net_job.reset(); + emitFailed(reason); +} + +/// @brief copy the matched blocked mods to the instance staging area +void PackInstallTask::copyBlockedMods() +{ + setStatus(tr("Copying Blocked Mods...")); + setAbortable(false); + int i = 0; + int total = m_blockedMods.length(); + setProgress(i, total); + for (const auto& mod : m_blockedMods) { + if (!mod.matched) { + qDebug() << mod.name << "was not matched to a local file, skipping copy"; + continue; + } + + auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", mod.targetFolder, mod.name); + + setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); + + qDebug() << "Will try to copy" << mod.localPath << "to" << dest_path; + + if (!FS::copy(mod.localPath, dest_path)()) { + qDebug() << "Copy of" << mod.localPath << "to" << dest_path << "Failed"; + } + + i++; + setProgress(i, total); + } + + setAbortable(true); +} + +} // namespace FTB diff --git a/launcher/modplatform/ftb/FTBPackInstallTask.h b/launcher/modplatform/ftb/FTBPackInstallTask.h new file mode 100644 index 000000000..49d2bb991 --- /dev/null +++ b/launcher/modplatform/ftb/FTBPackInstallTask.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020-2021 Petr Mrazek + * + * 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 "FTBPackManifest.h" + +#include "InstanceTask.h" +#include "QObjectPtr.h" +#include "modplatform/flame/FileResolvingTask.h" +#include "net/NetJob.h" +#include "ui/dialogs/BlockedModsDialog.h" + +#include + +namespace FTB { + +class PackInstallTask final : public InstanceTask { + Q_OBJECT + + public: + explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr); + ~PackInstallTask() override = default; + + bool abort() override; + + protected: + void executeTask() override; + + private slots: + void onManifestDownloadSucceeded(QByteArray* responsePtr); + void onResolveModsSucceeded(); + void onCreateInstanceSucceeded(); + void onModDownloadSucceeded(); + + void onManifestDownloadFailed(QString reason); + void onResolveModsFailed(QString reason); + void onCreateInstanceFailed(QString reason); + void onModDownloadFailed(QString reason); + + private: + void resolveMods(); + void createInstance(); + void downloadPack(); + void copyBlockedMods(); + + private: + NetJob::Ptr m_net_job = nullptr; + shared_qobject_ptr m_modIdResolverTask = nullptr; + + QList m_fileIds; + + Modpack m_pack; + QString m_versionName; + Version m_version; + + QMap m_filesToCopy; + QList m_blockedMods; + + // FIXME: nuke + QWidget* m_parent; +}; + +} // namespace FTB diff --git a/launcher/modplatform/ftb/FTBPackManifest.cpp b/launcher/modplatform/ftb/FTBPackManifest.cpp new file mode 100644 index 000000000..da633a117 --- /dev/null +++ b/launcher/modplatform/ftb/FTBPackManifest.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield + * Copyright 2020-2021 Petr Mrazek + * + * 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 "FTBPackManifest.h" + +#include "Json.h" + +static void loadSpecs(FTB::Specs& s, QJsonObject& obj) +{ + s.id = Json::requireInteger(obj, "id"); + s.minimum = Json::requireInteger(obj, "minimum"); + s.recommended = Json::requireInteger(obj, "recommended"); +} + +static void loadTag(FTB::Tag& t, QJsonObject& obj) +{ + t.id = Json::requireInteger(obj, "id"); + t.name = Json::requireString(obj, "name"); +} + +static void loadArt(FTB::Art& a, QJsonObject& obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.url = Json::requireString(obj, "url"); + a.type = Json::requireString(obj, "type"); + a.width = Json::requireInteger(obj, "width"); + a.height = Json::requireInteger(obj, "height"); + a.compressed = Json::requireBoolean(obj, "compressed"); + a.sha1 = Json::requireString(obj, "sha1"); + a.size = obj["size"].toInt(); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadAuthor(FTB::Author& a, QJsonObject& obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.website = Json::requireString(obj, "website"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionInfo(FTB::VersionInfo& v, QJsonObject& obj) +{ + v.id = Json::requireInteger(obj, "id"); + v.name = Json::requireString(obj, "name"); + v.type = Json::requireString(obj, "type"); + v.updated = Json::requireInteger(obj, "updated"); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(v.specs, specs); +} + +void FTB::loadModpack(FTB::Modpack& m, QJsonObject& obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.name = Json::requireString(obj, "name"); + m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; + m.synopsis = Json::requireString(obj, "synopsis"); + m.description = Json::requireString(obj, "description"); + m.type = Json::requireString(obj, "type"); + m.featured = Json::requireBoolean(obj, "featured"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = obj["refreshed"].toInt(); + auto artArr = Json::requireArray(obj, "art"); + for (QJsonValueRef artRaw : artArr) { + auto artObj = Json::requireObject(artRaw); + FTB::Art art; + loadArt(art, artObj); + m.art.append(art); + } + auto authorArr = Json::requireArray(obj, "authors"); + for (QJsonValueRef authorRaw : authorArr) { + auto authorObj = Json::requireObject(authorRaw); + FTB::Author author; + loadAuthor(author, authorObj); + m.authors.append(author); + } + auto versionArr = Json::requireArray(obj, "versions"); + for (QJsonValueRef versionRaw : versionArr) { + auto versionObj = Json::requireObject(versionRaw); + FTB::VersionInfo version; + loadVersionInfo(version, versionObj); + m.versions.append(version); + } + auto tagArr = Json::requireArray(obj, "tags"); + for (QJsonValueRef tagRaw : tagArr) { + auto tagObj = Json::requireObject(tagRaw); + FTB::Tag tag; + loadTag(tag, tagObj); + m.tags.append(tag); + } + m.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionTarget(FTB::VersionTarget& a, QJsonObject& obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.version = Json::requireString(obj, "version"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionFile(FTB::VersionFile& a, QJsonObject& obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.type = Json::requireString(obj, "type"); + a.path = Json::requireString(obj, "path"); + a.name = Json::requireString(obj, "name"); + a.version = Json::requireString(obj, "version"); + a.url = obj["url"].toString(); // optional + a.sha1 = Json::requireString(obj, "sha1"); + a.size = obj["size"].toInt(); + a.clientOnly = Json::requireBoolean(obj, "clientonly"); + a.serverOnly = Json::requireBoolean(obj, "serveronly"); + a.optional = Json::requireBoolean(obj, "optional"); + a.updated = Json::requireInteger(obj, "updated"); + auto curseforgeObj = obj["curseforge"].toObject(); // optional + a.curseforge.project_id = curseforgeObj["project"].toInt(); + a.curseforge.file_id = curseforgeObj["file"].toInt(); +} + +void FTB::loadVersion(FTB::Version& m, QJsonObject& obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.parent = Json::requireInteger(obj, "parent"); + m.name = Json::requireString(obj, "name"); + m.type = Json::requireString(obj, "type"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = obj["refreshed"].toInt(); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(m.specs, specs); + auto targetArr = Json::requireArray(obj, "targets"); + for (QJsonValueRef targetRaw : targetArr) { + auto versionObj = Json::requireObject(targetRaw); + FTB::VersionTarget target; + loadVersionTarget(target, versionObj); + m.targets.append(target); + } + auto fileArr = Json::requireArray(obj, "files"); + for (QJsonValueRef fileRaw : fileArr) { + auto fileObj = Json::requireObject(fileRaw); + FTB::VersionFile file; + loadVersionFile(file, fileObj); + m.files.append(file); + } +} diff --git a/launcher/modplatform/ftb/FTBPackManifest.h b/launcher/modplatform/ftb/FTBPackManifest.h new file mode 100644 index 000000000..704bde3e5 --- /dev/null +++ b/launcher/modplatform/ftb/FTBPackManifest.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2020 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace FTB { + +struct Specs { + int id; + int minimum; + int recommended; +}; + +struct Tag { + int id; + QString name; +}; + +struct Art { + int id; + QString url; + QString type; + int width; + int height; + bool compressed; + QString sha1; + int size; + int64_t updated; +}; + +struct Author { + int id; + QString name; + QString type; + QString website; + int64_t updated; +}; + +struct VersionInfo { + int id; + QString name; + QString type; + int64_t updated; + Specs specs; +}; + +struct Modpack { + int id; + QString name; + QString synopsis; + QString description; + QString type; + bool featured; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + QVector art; + QVector authors; + QVector versions; + QVector tags; + QString safeName; +}; + +struct VersionTarget { + int id; + QString type; + QString name; + QString version; + int64_t updated; +}; + +struct VersionFileCurseForge { + int project_id; + int file_id; +}; + +struct VersionFile { + int id; + QString type; + QString path; + QString name; + QString version; + QString url; + QString sha1; + int size; + bool clientOnly; + bool serverOnly; + bool optional; + int64_t updated; + VersionFileCurseForge curseforge; +}; + +struct Version { + int id; + int parent; + QString name; + QString type; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + Specs specs; + QVector targets; + QVector files; +}; + +struct VersionChangelog { + QString content; + int64_t updated; +}; + +void loadModpack(Modpack& m, QJsonObject& obj); + +void loadVersion(Version& m, QJsonObject& obj); +} // namespace FTB + +Q_DECLARE_METATYPE(FTB::Modpack) diff --git a/launcher/modplatform/helpers/ExportToModList.h b/launcher/modplatform/helpers/ExportToModList.h index ab7797fe6..7cbe730f2 100644 --- a/launcher/modplatform/helpers/ExportToModList.h +++ b/launcher/modplatform/helpers/ExportToModList.h @@ -23,7 +23,9 @@ namespace ExportToModList { enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; -enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; +enum OptionalDataValue { None = 0, Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; +Q_DECLARE_FLAGS(OptionalData, OptionalDataValue) + QString exportToModList(QList mods, Formats format, OptionalData extraData); QString exportToModList(QList mods, QString lineTemplate); } // namespace ExportToModList diff --git a/launcher/modplatform/helpers/OverrideUtils.cpp b/launcher/modplatform/helpers/OverrideUtils.cpp index d5958a59d..e64b30ffe 100644 --- a/launcher/modplatform/helpers/OverrideUtils.cpp +++ b/launcher/modplatform/helpers/OverrideUtils.cpp @@ -16,7 +16,7 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS QFile file(file_path); if (!file.open(QFile::WriteOnly)) { - qWarning() << "Failed to open file '" << file.fileName() << "' for writing!"; + qWarning() << "Failed to open file" << file.fileName() << "for writing:" << file.errorString(); return; } @@ -47,7 +47,7 @@ QStringList readOverrides(const QString& name, const QString& parent_folder) QStringList previous_overrides; if (!file.open(QFile::ReadOnly)) { - qWarning() << "Failed to open file '" << file.fileName() << "' for reading!"; + qWarning() << "Failed to open file" << file.fileName() << "for reading:" << file.errorString(); return previous_overrides; } diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index c9e8be34b..659c5d2ed 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -49,66 +49,55 @@ void PackInstallTask::copySettings() { setStatus(tr("Copying settings...")); progress(2, 2); + QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(instanceConfigPath); - instanceSettings->suspendSave(); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - instance.settings()->set("InstanceType", "OneSix"); - instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); + MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); - if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { - instance.settings()->set("OverrideJavaArgs", true); - instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); - } + { + SettingsObject::Lock lock(instance.settings()); + instance.settings()->set("InstanceType", "OneSix"); + instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); - auto components = instance.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); - - auto modloader = m_pack.loaderType; - if (modloader.has_value()) - switch (modloader.value()) { - case ModPlatform::NeoForge: { - components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); - break; - } - case ModPlatform::Forge: { - components->setComponentVersion("net.minecraftforge", m_pack.loaderVersion, true); - break; - } - case ModPlatform::Fabric: { - components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.loaderVersion, true); - break; - } - case ModPlatform::Quilt: { - components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true); - break; - } - case ModPlatform::Cauldron: - break; - case ModPlatform::LiteLoader: - break; - case ModPlatform::DataPack: - break; - case ModPlatform::Babric: - break; - case ModPlatform::BTA: - break; - case ModPlatform::LegacyFabric: - break; - case ModPlatform::Ornithe: - break; - case ModPlatform::Rift: - break; + if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { + instance.settings()->set("OverrideJavaArgs", true); + instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); } - components->saveNow(); - instance.setName(name()); - if (m_instIcon == "default") - m_instIcon = "ftb_logo"; - instance.setIconKey(m_instIcon); - instanceSettings->resumeSave(); + auto* components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + auto modloader = m_pack.loaderType; + if (modloader.has_value()) { + switch (modloader.value()) { + case ModPlatform::NeoForge: { + components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); + break; + } + case ModPlatform::Forge: { + components->setComponentVersion("net.minecraftforge", m_pack.loaderVersion, true); + break; + } + case ModPlatform::Fabric: { + components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.loaderVersion, true); + break; + } + case ModPlatform::Quilt: { + components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true); + break; + } + default: + break; + } + } + components->saveNow(); + + instance.setName(name()); + if (m_instIcon == "default") { + m_instIcon = "ftb_logo"; + } + instance.setIconKey(m_instIcon); + } emitSucceeded(); } diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index c5701da3c..7d1807ab3 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -53,13 +53,18 @@ void PackFetchTask::fetch() QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); - jobPtr->addNetAction(Net::ApiDownload::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); + + auto [publicAction, publicResponse] = Net::ApiDownload::makeByteArray(publicPacksUrl); + jobPtr->addNetAction(publicAction); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); - connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + auto [thirdPartyAction, thirdPartyResponse] = Net::Download::makeByteArray(thirdPartyUrl); + jobPtr->addNetAction(thirdPartyAction); + + connect(jobPtr.get(), &NetJob::succeeded, this, + [this, publicResponse, thirdPartyResponse] { fileDownloadFinished(publicResponse, thirdPartyResponse); }); connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); @@ -71,9 +76,10 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; for (auto& packCode : toFetch) { - auto data = std::make_shared(); NetJob* job = new NetJob("Fetching private pack", m_network); - job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + + auto [action, data] = Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode)); + job->addNetAction(action); job->setAskRetry(false); connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { @@ -85,20 +91,15 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) } job->deleteLater(); - - data->clear(); }); - connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { + connect(job, &NetJob::failed, this, [this, job, packCode](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); - - data->clear(); }); - connect(job, &NetJob::aborted, this, [this, job, data] { + connect(job, &NetJob::aborted, this, [this, job] { job->deleteLater(); - data->clear(); emit aborted(); }); @@ -107,20 +108,21 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) } } -void PackFetchTask::fileDownloadFinished() +void PackFetchTask::fileDownloadFinished(QByteArray* publicPtr, QByteArray* thirdPartyPtr) { - jobPtr.reset(); - QStringList failedLists; - if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) { + if (!parseAndAddPacks(*publicPtr, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } - if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) { + if (!parseAndAddPacks(*thirdPartyPtr, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } + // NOTE(TheKodeToad): we don't want to reset the jobPtr earlier as it may invalidate the responses! + jobPtr.reset(); + if (failedLists.size() > 0) { emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); } else { @@ -139,7 +141,6 @@ bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, Modpac if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; - data.clear(); return false; } diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index 4c7a8f6aa..3e1035b79 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "PackHelpers.h" #include "net/NetJob.h" @@ -13,25 +12,22 @@ class PackFetchTask : public QObject { Q_OBJECT public: - PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; + PackFetchTask(QNetworkAccessManager* network) : QObject(nullptr), m_network(network) {}; virtual ~PackFetchTask() = default; void fetch(); void fetchPrivate(const QStringList& toFetch); private: - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; NetJob::Ptr jobPtr; - std::shared_ptr publicModpacksXmlFileData = std::make_shared(); - std::shared_ptr thirdPartyModpacksXmlFileData = std::make_shared(); - bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; ModpackList thirdPartyPacks; protected slots: - void fileDownloadFinished(); + void fileDownloadFinished(QByteArray* publicResponse, QByteArray* thirdPartyResponse); void fileDownloadFailed(QString reason); void fileDownloadAborted(); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 33c0c38b6..8220676fc 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -52,7 +52,7 @@ namespace LegacyFTB { -PackInstallTask::PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version) +PackInstallTask::PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version) { m_pack = pack; m_version = version; @@ -133,79 +133,79 @@ void PackInstallTask::install() } QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(instanceConfigPath); - instanceSettings->suspendSave(); + MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); + { + SettingsObject::Lock lock(instance.settings()); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); - auto components = instance.getPackProfile(); - components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); - bool fallback = true; + bool fallback = true; - // handle different versions - QFile packJson(m_stagingPath + "/minecraft/pack.json"); - QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); - if (packJson.exists()) { - if (packJson.open(QIODevice::ReadOnly | QIODevice::Text)) { - QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); - packJson.close(); + // handle different versions + QFile packJson(m_stagingPath + "/minecraft/pack.json"); + QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); + if (packJson.exists()) { + if (packJson.open(QIODevice::ReadOnly | QIODevice::Text)) { + QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); + packJson.close(); - // we only care about the libs - QJsonArray libs = doc.object().value("libraries").toArray(); + // we only care about the libs + QJsonArray libs = doc.object().value("libraries").toArray(); - for (const auto& value : libs) { - QString nameValue = value.toObject().value("name").toString(); - if (!nameValue.startsWith("net.minecraftforge")) { - continue; + for (const auto& value : libs) { + QString nameValue = value.toObject().value("name").toString(); + if (!nameValue.startsWith("net.minecraftforge")) { + continue; + } + + GradleSpecifier forgeVersion(nameValue); + + components->setComponentVersion("net.minecraftforge", + forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); + packJson.remove(); + fallback = false; + break; } - - GradleSpecifier forgeVersion(nameValue); - - components->setComponentVersion("net.minecraftforge", - forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); - packJson.remove(); - fallback = false; - break; + } else { + qWarning() << "Failed to open file" << packJson.fileName() << "for reading:" << packJson.errorString(); } - } else { - qWarning() << "Failed to open file '" << packJson.fileName() << "' for reading!"; - } - } - - if (jarmodDir.exists()) { - qDebug() << "Found jarmods, installing..."; - - QStringList jarmods; - for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { - qDebug() << "Jarmod:" << info.fileName(); - jarmods.push_back(info.absoluteFilePath()); } - components->installJarMods(jarmods); - fallback = false; + if (jarmodDir.exists()) { + qDebug() << "Found jarmods, installing..."; + + QStringList jarmods; + for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { + qDebug() << "Jarmod:" << info.fileName(); + jarmods.push_back(info.absoluteFilePath()); + } + + components->installJarMods(jarmods); + fallback = false; + } + + // just nuke unzip directory, it s not needed anymore + FS::deletePath(m_stagingPath + "/unzip"); + + if (fallback) { + // TODO: Some fallback mechanism... or just keep failing! + emitFailed(tr("No installation method found!")); + return; + } + + components->saveNow(); + + progress(4, 4); + + instance.setName(name()); + if (m_instIcon == "default") { + m_instIcon = "ftb_logo"; + } + instance.setIconKey(m_instIcon); } - // just nuke unzip directory, it s not needed anymore - FS::deletePath(m_stagingPath + "/unzip"); - - if (fallback) { - // TODO: Some fallback mechanism... or just keep failing! - emitFailed(tr("No installation method found!")); - return; - } - - components->saveNow(); - - progress(4, 4); - - instance.setName(name()); - if (m_instIcon == "default") { - m_instIcon = "ftb_logo"; - } - instance.setIconKey(m_instIcon); - instanceSettings->resumeSave(); - emitSucceeded(); } diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index 6db6cb712..98777214f 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -14,7 +14,7 @@ class PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version); + explicit PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version); virtual ~PackInstallTask() {} bool canAbort() const override { return true; } @@ -35,7 +35,7 @@ class PackInstallTask : public InstanceTask { void onUnzipCanceled(); private: /* data */ - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; bool abortable = false; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 83d7521f0..d5bea52bc 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -9,19 +9,19 @@ #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" -#include "net/Upload.h" -Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) +std::pair ModrinthAPI::currentVersion(const QString& hash, const QString& hash_format) const { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); + auto [action, response] = + Net::ApiDownload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format)); + netJob->addNetAction(action); - return netJob; + return { netJob, response }; } -Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) +std::pair ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format) const { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); @@ -33,23 +33,24 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), body_raw); + netJob->addNetAction(action); netJob->setAskRetry(false); - return netJob; + return { netJob, response }; } -Task::Ptr ModrinthAPI::latestVersion(QString hash, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) +std::pair ModrinthAPI::latestVersion(const QString& hash, + const QString& hash_format, + std::optional> mcVersions, + std::optional loaders) const { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); QJsonObject body_obj; - if (loaders.has_value()) + if (loaders.has_value()) { Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); + } if (mcVersions.has_value()) { QStringList game_versions; @@ -62,17 +63,17 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::ApiUpload::makeByteArray( - QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); + auto [action, response] = Net::ApiUpload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), body_raw); + netJob->addNetAction(action); - return netJob; + return { netJob, response }; } -Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) +std::pair ModrinthAPI::latestVersions(const QStringList& hashes, + const QString& hash_format, + std::optional> mcVersions, + std::optional loaders) const { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -81,8 +82,9 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); - if (loaders.has_value()) + if (loaders.has_value()) { Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); + } if (mcVersions.has_value()) { QStringList game_versions; @@ -94,46 +96,48 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QJsonDocument body(body_obj); auto body_raw = body.toJson(); + auto [action, response] = Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), body_raw); + netJob->addNetAction(action); - netJob->addNetAction( - Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); - - return netJob; + return { netJob, response }; } -Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +std::pair ModrinthAPI::getProjects(QStringList addonIds) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(searchUrl)); + netJob->addNetAction(action); - return netJob; + return { netJob, response }; } QList ModrinthAPI::getSortingMethods() const { // https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects - return { { 1, "relevance", QObject::tr("Sort by Relevance") }, - { 2, "downloads", QObject::tr("Sort by Downloads") }, - { 3, "follows", QObject::tr("Sort by Follows") }, - { 4, "newest", QObject::tr("Sort by Newest") }, - { 5, "updated", QObject::tr("Sort by Last Updated") } }; + return { { .index = 1, .name = "relevance", .readable_name = QObject::tr("Sort by Relevance") }, + { .index = 2, .name = "downloads", .readable_name = QObject::tr("Sort by Downloads") }, + { .index = 3, .name = "follows", .readable_name = QObject::tr("Sort by Follows") }, + { .index = 4, .name = "newest", .readable_name = QObject::tr("Sort by Newest") }, + { .index = 5, .name = "updated", .readable_name = QObject::tr("Sort by Last Updated") } }; } -Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) +std::pair ModrinthAPI::getModCategories() { auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response)); - QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); - return netJob; + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category")); + netJob->addNetAction(action); + QObject::connect(netJob.get(), &Task::failed, [](const QString& msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); + + return { netJob, response }; } -QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) +QList ModrinthAPI::loadCategories(const QByteArray& response, const QString& projectType) { QList categories; QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); @@ -147,8 +151,9 @@ QList ModrinthAPI::loadCategories(std::shared_ptr ModrinthAPI::loadCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr response) +QList ModrinthAPI::loadModCategories(const QByteArray& response) { return loadCategories(response, "mod"); }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 1990350d4..731ac1b09 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -5,68 +5,66 @@ #pragma once #include "BuildConfig.h" -#include "Json.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" #include +#include class ModrinthAPI : public ResourceAPI { public: - Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr response); + std::pair currentVersion(const QString& hash, const QString& hash_format) const; - Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response); + std::pair currentVersions(const QStringList& hashes, QString hash_format) const; - Task::Ptr latestVersion(QString hash, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response); + std::pair latestVersion(const QString& hash, + const QString& hash_format, + std::optional> mcVersions, + std::optional loaders) const; - Task::Ptr latestVersions(const QStringList& hashes, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response); + std::pair latestVersions(const QStringList& hashes, + const QString& hash_format, + std::optional> mcVersions, + std::optional loaders) const; - Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + std::pair getProjects(QStringList addonIds) const override; - static Task::Ptr getModCategories(std::shared_ptr response); - static QList loadCategories(std::shared_ptr response, QString projectType); - static QList loadModCategories(std::shared_ptr response); + static std::pair getModCategories(); + static QList loadCategories(const QByteArray& response, const QString& projectType); + static QList loadModCategories(const QByteArray& response); public: auto getSortingMethods() const -> QList override; - inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; + static auto getAuthorURL(const QString& name) -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> QStringList { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) { - if (types & loader) { + if ((types & loader) != 0U) { l << getModLoaderAsString(loader); } } return l; } - static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> QString { QStringList l; - for (auto loader : getModLoaderStrings(types)) { + for (const auto& loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } return l.join(','); } - static auto getCategoriesFilters(QStringList categories) -> const QString + static auto getCategoriesFilters(const QStringList& categories) -> QString { QStringList l; - for (auto cat : categories) { + for (const auto& cat : categories) { l << QString("\"categories:%1\"").arg(cat); } return l.join(','); @@ -76,11 +74,11 @@ class ModrinthAPI : public ResourceAPI { { switch (side) { case ModPlatform::Side::ClientSide: - return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\""); + return { R"("client_side:required","client_side:optional"],["server_side:optional","server_side:unsupported")" }; case ModPlatform::Side::ServerSide: - return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\""); + return { R"("server_side:required","server_side:optional"],["client_side:optional","client_side:unsupported")" }; case ModPlatform::Side::UniversalSide: - return QString("\"client_side:required\"],[\"server_side:required\""); + return { R"("client_side:required"],["server_side:required")" }; case ModPlatform::Side::NoSide: // fallthrough default: @@ -88,17 +86,17 @@ class ModrinthAPI : public ResourceAPI { } } - static inline QString mapMCVersionFromModrinth(QString v) + static QString mapMCVersionFromModrinth(QString v) { - static const QString preString = " Pre-Release "; + static const QString s_preString = " Pre-Release "; bool pre = false; if (v.contains("-pre")) { pre = true; - v.replace("-pre", preString); + v.replace("-pre", s_preString); } v.replace("-", " "); if (pre) { - v.replace(" Pre Release ", preString); + v.replace(" Pre Release ", s_preString); } return v; } @@ -125,23 +123,28 @@ class ModrinthAPI : public ResourceAPI { return ""; } - QString createFacets(SearchArgs const& args) const + QString createFacets(const SearchArgs& args) const { QStringList facets_list; - if (args.loaders.has_value() && args.loaders.value() != 0) + if (args.loaders.has_value() && args.loaders.value() != 0) { facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); - if (args.versions.has_value() && !args.versions.value().empty()) + } + if (args.versions.has_value() && !args.versions.value().empty()) { facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); + } if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); - if (!side.isEmpty()) + if (!side.isEmpty()) { facets_list.append(QString("[%1]").arg(side)); + } } - if (args.categoryIds.has_value() && !args.categoryIds->empty()) + if (args.categoryIds.has_value() && !args.categoryIds->empty()) { facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); - if (args.openSource) + } + if (args.openSource) { facets_list.append("[\"open_source:true\"]"); + } facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); @@ -149,7 +152,7 @@ class ModrinthAPI : public ResourceAPI { } public: - inline auto getSearchURL(SearchArgs const& args) const -> std::optional override + auto getSearchURL(const SearchArgs& args) const -> std::optional override { if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { @@ -161,67 +164,74 @@ class ModrinthAPI : public ResourceAPI { QStringList get_arguments; get_arguments.append(QString("offset=%1").arg(args.offset)); get_arguments.append(QString("limit=25")); - if (args.search.has_value()) + if (args.search.has_value()) { get_arguments.append(QString("query=%1").arg(args.search.value())); - if (args.sorting.has_value()) + } + if (args.sorting.has_value()) { get_arguments.append(QString("index=%1").arg(args.sorting.value().name)); + } get_arguments.append(QString("facets=%1").arg(createFacets(args))); return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); }; - inline auto getInfoURL(QString const& id) const -> std::optional override + auto getInfoURL(const QString& id) const -> std::optional override { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; - inline auto getMultipleModInfoURL(QStringList ids) const -> QString + auto getMultipleModInfoURL(const QStringList& ids) const -> QString { return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); }; - inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional override + auto getVersionsURL(const VersionSearchArgs& args) const -> std::optional override { QStringList get_arguments; - if (args.mcVersions.has_value()) + if (args.mcVersions.has_value()) { get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value()))); - if (args.loaders.has_value()) + } + if (args.loaders.has_value()) { get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); + } + get_arguments.append(QString("include_changelog=%1").arg(args.includeChangelog ? "true" : "false")); return QString("%1/project/%2/version%3%4") .arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; - QString getGameVersionsArray(std::list mcVersions) const + QString getGameVersionsArray(const std::vector& mcVersions) const { QString s; - for (auto& ver : mcVersions) { - s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver)); + for (const auto& ver : mcVersions) { + s += QString(R"("versions:%1",)").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; } - static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + static auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { - return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | - ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | - ModPlatform::Ornithe | ModPlatform::Rift); + return (loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | + ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | + ModPlatform::Ornithe | ModPlatform::Rift)) != 0; } - std::optional getDependencyURL(DependencySearchArgs const& args) const override + std::optional getDependencyURL(const DependencySearchArgs& args) const override { - return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) - : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") - .arg(BuildConfig.MODRINTH_PROD_URL) - .arg(args.dependency.addonId.toString()) - .arg(mapMCVersionToModrinth(args.mcVersion)) - .arg(getModLoaderStrings(args.loader).join("\",\"")); + return args.dependency.version.length() != 0 + ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) + : QString(R"(%1/project/%2/version?game_versions=["%3"]&loaders=["%4"]&include_changelog=%5)") + .arg(BuildConfig.MODRINTH_PROD_URL) + .arg(args.dependency.addonId.toString()) + .arg(mapMCVersionToModrinth(args.mcVersion)) + .arg(getModLoaderStrings(args.loader).join("\",\"")) + .arg(args.includeChangelog ? "true" : "false"); }; QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); } - ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType /*unused*/) const override { return Modrinth::loadIndexedPackVersion(obj); }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index c81d1fc18..0fda37e57 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -13,23 +13,23 @@ #include "tasks/ConcurrentTask.h" -static ModrinthAPI api; +static const ModrinthAPI g_api; ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, - std::list& mcVersions, + std::vector& mcVersions, QList loadersList, - std::shared_ptr resourceModel) - : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) + ResourceFolderModel* resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), resourceModel) , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) { if (!m_loadersList.isEmpty()) { // this is for mods so append all the other posible loaders to the initial list m_initialSize = m_loadersList.length(); ModPlatform::ModLoaderTypes modLoaders; - for (auto m : resources) { + for (auto* m : resources) { modLoaders |= m->metadata()->loaders; } for (auto l : m_loadersList) { - modLoaders &= ~l; + modLoaders &= ~static_cast(l); } m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); } @@ -37,8 +37,9 @@ ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, bool ModrinthCheckUpdate::abort() { - if (m_job) + if (m_job) { return m_job->abort(); + } return true; } @@ -50,9 +51,9 @@ bool ModrinthCheckUpdate::abort() void ModrinthCheckUpdate::executeTask() { setStatus(tr("Preparing resources for Modrinth...")); - setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1); + setProgress(0, ((m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2) + 1); - auto hashing_task = + auto hashingTask = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); bool startHasing = false; for (auto* resource : m_resources) { @@ -62,10 +63,11 @@ void ModrinthCheckUpdate::executeTask() // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) if (resource->metadata()->hash_format != m_hashType) { - auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); - connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); - hashing_task->addTask(hash_task); + auto hashTask = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, + [this, resource](const QString& hash) { m_mappings.insert(hash, resource); }); + connect(hashTask.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); + hashingTask->addTask(hashTask); startHasing = true; } else { m_mappings.insert(hash, resource); @@ -73,9 +75,9 @@ void ModrinthCheckUpdate::executeTask() } if (startHasing) { - connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); - m_job = hashing_task; - hashing_task->start(); + connect(hashingTask.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); + m_job = hashingTask; + hashingTask->start(); } else { checkNextLoader(); } @@ -88,11 +90,10 @@ void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional(); QStringList hashes; if (forceModLoaderCheck && loader.has_value()) { - for (auto hash : m_mappings.keys()) { - if (m_mappings.value(hash)->metadata()->loaders & loader.value()) { + for (const auto& hash : m_mappings.keys()) { + if ((m_mappings.value(hash)->metadata()->loaders & loader.value()) != 0) { hashes.append(hash); } } @@ -105,7 +106,7 @@ void ModrinthCheckUpdate::getUpdateModsForLoader(std::optionalstart(); } -void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, std::optional loader) +void ModrinthCheckUpdate::checkVersionsResponse(QByteArray* response, std::optional loader) { setStatus(tr("Parsing the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parse_error.offset - << "reason:" << parse_error.errorString(); + QJsonParseError parseError{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parseError.offset + << "reason:" << parseError.errorString(); qWarning() << *response; - emitFailed(parse_error.errorString()); + emitFailed(parseError.errorString()); return; } @@ -138,11 +139,11 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp const QString hash = iter.key(); Resource* resource = iter.value(); - auto project_obj = doc[hash].toObject(); + auto projectObj = doc[hash].toObject(); // If the returned project is empty, but we have Modrinth metadata, // it means this specific version is not available - if (project_obj.isEmpty()) { + if (projectObj.isEmpty()) { qDebug() << "Mod" << m_mappings.find(hash).value()->name() << "got an empty response. Hash:" << hash; ++iter; continue; @@ -150,11 +151,11 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it - QString loader_filter; - if (loader.has_value()) { - for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) { - loader_filter = ModPlatform::getModLoaderAsString(flag); - break; + QString loaderFilter; + if (loader.has_value() && loader != 0) { + auto modLoaders = ModPlatform::modLoaderTypesToList(*loader); + if (!modLoaders.isEmpty()) { + loaderFilter = ModPlatform::getModLoaderAsString(modLoaders.first()); } } @@ -164,9 +165,9 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // Such is the pain of having arbitrary files for a given version .-. - auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter); - if (project_ver.downloadUrl.isEmpty()) { - qCritical() << "Modrinth mod without download url!" << project_ver.fileName; + auto projectVer = Modrinth::loadIndexedPackVersion(projectObj, m_hashType, loaderFilter); + if (projectVer.downloadUrl.isEmpty()) { + qCritical() << "Modrinth mod without download url!" << projectVer.fileName; ++iter; continue; } @@ -177,21 +178,22 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; - if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { - auto download_task = makeShared(pack, project_ver, m_resourceModel); + if ((projectVer.hash != hash && projectVer.is_preferred) || (resource->status() == ResourceStatus::NotInstalled)) { + auto downloadTask = makeShared(pack, projectVer, m_resourceModel, true, "update"); - QString old_version = resource->metadata()->version_number; - if (old_version.isEmpty()) { - if (resource->status() == ResourceStatus::NOT_INSTALLED) - old_version = tr("Not installed"); - else - old_version = tr("Unknown"); + QString oldVersion = resource->metadata()->version_number; + if (oldVersion.isEmpty()) { + if (resource->status() == ResourceStatus::NotInstalled) { + oldVersion = tr("Not installed"); + } else { + oldVersion = tr("Unknown"); + } } - m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, - project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, resource->enabled()); + m_updates.emplace_back(pack->name, hash, oldVersion, projectVer.version_number, projectVer.version_type, + projectVer.changelog, ModPlatform::ResourceProvider::MODRINTH, downloadTask, resource->enabled()); } - m_deps.append(std::make_shared(pack, project_ver)); + m_deps.append(std::make_shared(pack, projectVer)); iter = m_mappings.erase(iter); } @@ -211,23 +213,25 @@ void ModrinthCheckUpdate::checkNextLoader() if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); return; - } else if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader + } + if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader getUpdateModsForLoader(); return; } - for (auto resource : m_mappings) { + for (auto* resource : m_mappings) { QString reason; - if (dynamic_cast(resource) != nullptr) + if (dynamic_cast(resource) != nullptr) { reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); - else + } else { reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); + } emit checkFailed(resource, reason); } emitSucceeded(); -} \ No newline at end of file +} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index eb8057694..c0407bef8 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -7,9 +7,9 @@ class ModrinthCheckUpdate : public CheckUpdateTask { public: ModrinthCheckUpdate(QList& resources, - std::list& mcVersions, + std::vector& mcVersions, QList loadersList, - std::shared_ptr resourceModel); + ResourceFolderModel* resourceModel); public slots: bool abort() override; @@ -17,7 +17,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { protected slots: void executeTask() override; void getUpdateModsForLoader(std::optional loader = {}, bool forceModLoaderCheck = false); - void checkVersionsResponse(std::shared_ptr response, std::optional loader); + void checkVersionsResponse(QByteArray* response, std::optional loader); void checkNextLoader(); private: @@ -25,5 +25,5 @@ class ModrinthCheckUpdate : public CheckUpdateTask { QHash m_mappings; QString m_hashType; int m_loaderIdx = 0; - int m_initialSize = 0; + qsizetype m_initialSize = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 767bb003f..0cb2c547d 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -16,7 +16,10 @@ #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" +#include "net/ApiHeaderProxy.h" #include "net/NetJob.h" + +#include "modplatform/ModIndex.h" #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" @@ -29,129 +32,119 @@ bool ModrinthCreationTask::abort() { - if (!canAbort()) + if (!canAbort()) { return false; + } - m_abort = true; - if (m_task) + if (m_task) { m_task->abort(); - return Task::abort(); + } + return InstanceCreationTask::abort(); } bool ModrinthCreationTask::updateInstance() { - auto instance_list = APPLICATION->instances(); + auto* instanceList = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - InstancePtr inst; - if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { - inst = instance_list->getInstanceById(original_id); + BaseInstance* inst = nullptr; + if (auto originalId = originalInstanceID(); !originalId.isEmpty()) { + inst = instanceList->getInstanceById(originalId); Q_ASSERT(inst); } else { - inst = instance_list->getInstanceByManagedName(originalName()); + inst = instanceList->getInstanceByManagedName(originalName()); if (!inst) { - inst = instance_list->getInstanceById(originalName()); + inst = instanceList->getInstanceById(originalName()); - if (!inst) + if (!inst) { return false; + } } } - QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); - if (!parseManifest(index_path, m_files, true, false)) + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (!parseManifest(indexPath, m_files, true, false)) { return false; + } - auto version_name = inst->getManagedPackVersionName(); + auto versionName = inst->getManagedPackVersionName(); m_root_path = QFileInfo(inst->gameRoot()).fileName(); - auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; + auto versionStr = !versionName.isEmpty() ? tr(" (version %1)").arg(versionName) : ""; if (shouldConfirmUpdate()) { - auto should_update = askIfShouldUpdate(m_parent, version_str); - if (should_update == ShouldUpdate::SkipUpdating) + auto shouldUpdate = askIfShouldUpdate(m_parent, versionStr); + if (shouldUpdate == ShouldUpdate::SkipUpdating) { return false; - if (should_update == ShouldUpdate::Cancel) { + } + if (shouldUpdate == ShouldUpdate::Cancel) { m_abort = true; return false; } } // Remove repeated files, we don't need to download them! - QDir old_inst_dir(inst->instanceRoot()); + QDir oldInstDir(inst->instanceRoot()); - QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack")); + QString oldIndexFolder(FS::PathCombine(oldInstDir.absolutePath(), "mrpack")); - QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); - QFileInfo old_index_file(old_index_path); - if (old_index_file.exists()) { - std::vector old_files; - parseManifest(old_index_path, old_files, false, false); + QString oldIndexPath(FS::PathCombine(oldIndexFolder, "modrinth.index.json")); + QFileInfo oldIndexFile(oldIndexPath); + if (oldIndexFile.exists()) { + std::vector oldFiles; + parseManifest(oldIndexPath, oldFiles, false, false); // Let's remove all duplicated, identical resources! - auto files_iterator = m_files.begin(); + auto filesIterator = m_files.begin(); begin: - while (files_iterator != m_files.end()) { - auto const& file = *files_iterator; + while (filesIterator != m_files.end()) { + const auto& file = *filesIterator; - auto old_files_iterator = old_files.begin(); - while (old_files_iterator != old_files.end()) { - auto const& old_file = *old_files_iterator; + auto oldFilesIterator = oldFiles.begin(); + while (oldFilesIterator != oldFiles.end()) { + const auto& oldFile = *oldFilesIterator; - if (old_file.hash == file.hash) { + if (oldFile.hash == file.hash) { qDebug() << "Removed file at" << file.path << "from list of downloads"; - files_iterator = m_files.erase(files_iterator); - old_files_iterator = old_files.erase(old_files_iterator); + filesIterator = m_files.erase(filesIterator); + oldFilesIterator = oldFiles.erase(oldFilesIterator); goto begin; // Sorry :c } - old_files_iterator++; + oldFilesIterator++; } - files_iterator++; + filesIterator++; } - QDir old_minecraft_dir(inst->gameRoot()); + QDir oldMinecraftDir(inst->gameRoot()); // Some files were removed from the old version, and some will be downloaded in an updated version, // so we're fine removing them! - if (!old_files.empty()) { - for (auto const& file : old_files) { - if (file.path.isEmpty()) - continue; - qDebug() << "Scheduling" << file.path << "for removal"; - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); - if (file.path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9))); - } else { - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled")); - } + if (!oldFiles.empty()) { + for (const auto& file : oldFiles) { + scheduleToDelete(m_parent, oldMinecraftDir, file.path, true); } } // We will remove all the previous overrides, to prevent duplicate files! // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // FIXME: We may want to do something about disabled mods. - auto old_overrides = Override::readOverrides("overrides", old_index_folder); - for (const auto& entry : old_overrides) { - if (entry.isEmpty()) - continue; - qDebug() << "Scheduling" << entry << "for removal"; - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + auto oldOverrides = Override::readOverrides("overrides", oldIndexFolder); + for (const auto& entry : oldOverrides) { + scheduleToDelete(m_parent, oldMinecraftDir, entry); } - auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); - for (const auto& entry : old_client_overrides) { - if (entry.isEmpty()) - continue; - qDebug() << "Scheduling" << entry << "for removal"; - m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); + auto oldClientOverrides = Override::readOverrides("client-overrides", oldIndexFolder); + for (const auto& entry : oldClientOverrides) { + scheduleToDelete(m_parent, oldMinecraftDir, entry); } } else { // We don't have an old index file, so we may duplicate stuff! - auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), - tr("We couldn't find a suitable index file for the older version. This may cause some " - "of the files to be duplicated. Do you want to continue?"), - QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); + auto* dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some " + "of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; @@ -169,130 +162,151 @@ bool ModrinthCreationTask::updateInstance() } // https://docs.modrinth.com/docs/modpacks/format_definition/ -bool ModrinthCreationTask::createInstance() +std::unique_ptr ModrinthCreationTask::createInstance() { QEventLoop loop; - QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack")); + QString parentFolder(FS::PathCombine(m_stagingPath, "mrpack")); - QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); - if (m_files.empty() && !parseManifest(index_path, m_files, true, true)) - return false; + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + if (m_files.empty() && !parseManifest(indexPath, m_files, true, true)) { + return nullptr; + } // Keep index file in case we need it some other time (like when changing versions) - QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); - FS::ensureFilePathExists(new_index_place); - FS::move(index_path, new_index_place); + QString newIndexPlace(FS::PathCombine(parentFolder, "modrinth.index.json")); + FS::ensureFilePathExists(newIndexPlace); + FS::move(indexPath, newIndexPlace); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); - auto override_path = FS::PathCombine(m_stagingPath, "overrides"); - if (QFile::exists(override_path)) { + auto overridePath = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(overridePath)) { // Create a list of overrides in "overrides.txt" inside mrpack/ - Override::createOverrides("overrides", parent_folder, override_path); + Override::createOverrides("overrides", parentFolder, overridePath); // Apply the overrides - if (!FS::move(override_path, mcPath)) { + if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + "overrides"); - return false; + return nullptr; } } // Do client overrides - auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); - if (QFile::exists(client_override_path)) { + auto clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(clientOverridePath)) { // Create a list of overrides in "client-overrides.txt" inside mrpack/ - Override::createOverrides("client-overrides", parent_folder, client_override_path); + Override::createOverrides("client-overrides", parentFolder, clientOverridePath); // Apply the overrides - if (!FS::overrideFolder(mcPath, client_override_path)) { + if (!FS::overrideFolder(mcPath, clientOverridePath)) { setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); - return false; + return nullptr; } } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(configPath); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto instanceSettings = std::make_unique(configPath); + auto instance = std::make_unique(m_globalSettings, std::move(instanceSettings), m_stagingPath); - auto components = instance.getPackProfile(); + auto* components = instance->getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_minecraft_version, true); - if (!m_fabric_version.isEmpty()) + QString loader; + if (!m_fabric_version.isEmpty()) { components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); - if (!m_quilt_version.isEmpty()) + loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Fabric); + } + if (!m_quilt_version.isEmpty()) { components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); - if (!m_forge_version.isEmpty()) + loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Quilt); + } + if (!m_forge_version.isEmpty()) { components->setComponentVersion("net.minecraftforge", m_forge_version); - if (!m_neoForge_version.isEmpty()) + loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::Forge); + } + if (!m_neoForge_version.isEmpty()) { components->setComponentVersion("net.neoforged", m_neoForge_version); + loader = ModPlatform::getModLoaderAsString(ModPlatform::ModLoaderType::NeoForge); + } if (m_instIcon != "default") { - instance.setIconKey(m_instIcon); + instance->setIconKey(m_instIcon); } else if (!m_managed_id.isEmpty()) { - instance.setIconKey("modrinth"); + instance->setIconKey("modrinth"); } // Don't add managed info to packs without an ID (most likely imported from ZIP) - if (!m_managed_id.isEmpty()) - instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); - else - instance.setManagedPack("modrinth", "", name(), "", ""); + if (!m_managed_id.isEmpty()) { + instance->setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); + } else { + instance->setManagedPack("modrinth", "", name(), "", ""); + } - instance.setName(name()); - instance.saveNow(); + instance->setName(name()); + instance->saveNow(); auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); - auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); - auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); + auto rootModpackPath = FS::PathCombine(m_stagingPath, m_root_path); + auto rootModpackUrl = QUrl::fromLocalFile(rootModpackPath); // TODO make this work with other sorts of resource QHash resources; for (auto& file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); - auto file_path = FS::PathCombine(root_modpack_path, fileName); - if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) { + auto filePath = FS::PathCombine(rootModpackPath, fileName); + if (!rootModpackUrl.isParentOf(QUrl::fromLocalFile(filePath))) { // This means we somehow got out of the root folder, so abort here to prevent exploits setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.") .arg(fileName)); - return false; + return nullptr; } if (fileName.startsWith("mods/")) { - auto mod = new Mod(file_path); + auto* mod = new Mod(filePath); ModDetails d; - d.mod_id = file_path; + d.mod_id = filePath; mod->setDetails(d); resources[file.hash.toHex()] = mod; } if (file.downloads.empty()) { setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); - return false; + return nullptr; } - qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; - auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); + qDebug() << "Will try to download" << file.downloads.front() << "to" << filePath; + + Net::ModrinthDownloadMeta meta{ + .reason = m_instance.has_value() ? "update" : "modpack", + .gameVersion = m_minecraft_version, + .loader = loader, + }; + + QUrl downloadUrl = file.downloads.dequeue(); + auto dl = Net::ApiDownload::makeFile(downloadUrl, filePath, Net::Download::Option::NoOptions, meta); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); - connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] { - auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); + connect(dl.get(), &Task::failed, [&file, filePath, param, downloadMods, meta] { + QUrl fallbackUrl = file.downloads.dequeue(); + auto ndl = Net::ApiDownload::makeFile(fallbackUrl, filePath, Net::Download::Option::NoOptions, meta); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(ndl); - if (auto shared = param.lock()) + if (auto shared = param.lock()) { shared->succeeded(); + } }); } } - bool ended_well = false; + bool endedWell = false; - connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); - connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](const QString& reason) { - ended_well = false; + connect(downloadMods.get(), &NetJob::succeeded, this, [&endedWell]() { endedWell = true; }); + connect(downloadMods.get(), &NetJob::failed, [this, &endedWell](const QString& reason) { + endedWell = false; setError(reason); }); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); @@ -308,17 +322,17 @@ bool ModrinthCreationTask::createInstance() loop.exec(); - if (!ended_well) { - for (auto resource : resources) { + if (!endedWell) { + for (auto* resource : resources) { delete resource; } - return ended_well; + return nullptr; } QEventLoop ensureMetaLoop; - QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); + QDir folder = FS::PathCombine(instance->modsRoot(), ".index"); auto ensureMetadataTask = makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); - connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; }); + connect(ensureMetadataTask.get(), &Task::succeeded, this, [&endedWell]() { endedWell = true; }); connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); @@ -330,37 +344,38 @@ bool ModrinthCreationTask::createInstance() m_task = ensureMetadataTask; ensureMetaLoop.exec(); - for (auto resource : resources) { + for (auto* resource : resources) { delete resource; } resources.clear(); // Update information of the already installed instance, if any. - if (m_instance && ended_well) { + if (m_instance && endedWell) { setAbortable(false); - auto inst = m_instance.value(); + auto* inst = m_instance.value(); // Only change the name if it didn't use a custom name, so that the previous custom name // is preserved, but if we're using the original one, we update the version string. // NOTE: This needs to come before the copyManagedPack call! - if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name()) { - if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) - inst->setName(instance.name()); + if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance->name()) { + if (askForChangingInstanceName(m_parent, inst->name(), instance->name()) == InstanceNameChange::ShouldChange) { + inst->setName(instance->name()); + } } - inst->copyManagedPack(instance); + inst->copyManagedPack(*instance); } - return ended_well; + if (endedWell) { + return instance; + } + return nullptr; } -bool ModrinthCreationTask::parseManifest(const QString& index_path, - std::vector& files, - bool set_internal_data, - bool show_optional_dialog) +bool ModrinthCreationTask::parseManifest(const QString& indexPath, std::vector& files, bool setInternalData, bool showOptionalDialog) { try { - auto doc = Json::requireDocument(index_path); + auto doc = Json::requireDocument(indexPath); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); if (formatVersion == 1) { @@ -369,9 +384,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, throw JSONValidationError("Unknown game: " + game); } - if (set_internal_data) { - if (m_managed_version_id.isEmpty()) + if (setInternalData) { + if (m_managed_version_id.isEmpty()) { m_managed_version_id = obj["versionId"].toString(); + } m_managed_name = obj["name"].toString(); } @@ -387,7 +403,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, QString support = env["client"].toString("unsupported"); if (support == "unsupported") { continue; - } else if (support == "optional") { + } + if (support == "optional") { file.required = false; } } @@ -399,20 +416,21 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) - auto download_arr = modInfo["downloads"].toArray(); - for (auto download : download_arr) { + auto downloadArr = modInfo["downloads"].toArray(); + for (auto download : downloadArr) { qWarning() << download.toString(); - bool is_last = download.toString() == download_arr.last().toString(); + bool isLast = download.toString() == downloadArr.last().toString(); - auto download_url = QUrl(download.toString()); + auto downloadUrl = QUrl(download.toString()); - if (!download_url.isValid()) { + if (!downloadUrl.isValid()) { qDebug() - << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path); - if (is_last && file.downloads.isEmpty()) + << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(downloadUrl.toString(), file.path); + if (isLast && file.downloads.isEmpty()) { throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); + } } else { - file.downloads.push_back(download_url); + file.downloads.push_back(downloadUrl); } } @@ -420,10 +438,11 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } if (!optionalFiles.empty()) { - if (show_optional_dialog) { + if (showOptionalDialog) { QStringList oFiles; - for (auto file : optionalFiles) + for (const auto& file : optionalFiles) { oFiles.push_back(file.path); + } OptionalModDialog optionalModDialog(m_parent, oFiles); if (optionalModDialog.exec() == QDialog::Rejected) { emitAborted(); @@ -446,7 +465,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } } } - if (set_internal_data) { + if (setInternalData) { auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index e02a55877..c8835e142 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -24,27 +24,27 @@ class ModrinthCreationTask final : public InstanceCreationTask { }; public: - ModrinthCreationTask(QString staging_path, - SettingsObjectPtr global_settings, + ModrinthCreationTask(const QString& stagingPath, + SettingsObject* globalSettings, QWidget* parent, QString id, - QString version_id = {}, - QString original_instance_id = {}) - : InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id)) + QString versionId = {}, + QString originalInstanceId = {}) + : m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(versionId)) { - setStagingPath(staging_path); - setParentSettings(global_settings); + setStagingPath(stagingPath); + setParentSettings(globalSettings); - m_original_instance_id = std::move(original_instance_id); + m_original_instance_id = std::move(originalInstanceId); } bool abort() override; bool updateInstance() override; - bool createInstance() override; + std::unique_ptr createInstance() override; private: - bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); + bool parseManifest(const QString&, std::vector&, bool setInternalData = true, bool showOptionalDialog = true); private: QWidget* m_parent = nullptr; @@ -55,7 +55,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { std::vector m_files; Task::Ptr m_task; - std::optional m_instance; + std::optional m_instance; QString m_root_path = "minecraft"; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index c638e8db0..9a972bc85 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -40,7 +40,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, - InstancePtr instance, + BaseInstance* instance, const QString& output, MMCZip::FilterFileFunction filter) : name(name) @@ -48,7 +48,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, , summary(summary) , optionalFiles(optionalFiles) , instance(instance) - , mcInstance(dynamic_cast(instance.get())) + , mcInstance(dynamic_cast(instance)) , gameRoot(instance->gameRoot()) , output(output) , filter(filter) @@ -86,7 +86,7 @@ void ModrinthPackExportTask::collectFiles() if (mcInstance) { mcInstance->loaderModList()->update(); - connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes); + connect(mcInstance->loaderModList(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes); } else collectHashes(); } @@ -108,13 +108,13 @@ void ModrinthPackExportTask::collectHashes() QFile openFile(file.absoluteFilePath()); if (!openFile.open(QFile::ReadOnly)) { - qWarning() << "Could not open" << file << "for hashing"; + qWarning() << "Could not open" << file << "for hashing:" << openFile.errorString(); continue; } const QByteArray data = openFile.readAll(); if (openFile.error() != QFileDevice::NoError) { - qWarning() << "Could not read" << file; + qWarning() << "Could not read" << file << "error:" << openFile.errorString(); continue; } auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512); @@ -155,8 +155,8 @@ void ModrinthPackExportTask::makeApiRequest() buildZip(); else { setStatus(tr("Finding versions for hashes...")); - auto response = std::make_shared(); - task = api.currentVersions(pendingHashes.values(), "sha512", response); + auto [versionsTask, response] = api.currentVersions(pendingHashes.values(), "sha512"); + task = versionsTask; connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); @@ -164,7 +164,7 @@ void ModrinthPackExportTask::makeApiRequest() } } -void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) +void ModrinthPackExportTask::parseApiResponse(QByteArray* response) { task = nullptr; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index f9b86bbd7..5aca657a9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -34,7 +34,7 @@ class ModrinthPackExportTask : public Task { const QString& version, const QString& summary, bool optionalFiles, - InstancePtr instance, + BaseInstance* instance, const QString& output, MMCZip::FilterFileFunction filter); @@ -55,7 +55,7 @@ class ModrinthPackExportTask : public Task { // inputs const QString name, version, summary; const bool optionalFiles; - const InstancePtr instance; + const BaseInstance* instance; MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; @@ -70,7 +70,7 @@ class ModrinthPackExportTask : public Task { void collectFiles(); void collectHashes(); void makeApiRequest(); - void parseApiResponse(std::shared_ptr response); + void parseApiResponse(QByteArray* response); void buildZip(); QByteArray generateIndex(); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 699e12b40..48d28feee 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -26,9 +26,7 @@ #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" -static ModrinthAPI api; - -bool shouldDownloadOnSide(QString side) +bool shouldDownloadOnSide(const QString& side) { return side == "required" || side == "optional"; } @@ -37,17 +35,19 @@ bool shouldDownloadOnSide(QString side) void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = obj["project_id"].toString(); - if (pack.addonId.toString().isEmpty()) + if (pack.addonId.toString().isEmpty()) { pack.addonId = Json::requireString(obj, "id"); + } pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.name = Json::requireString(obj, "title"); pack.slug = obj["slug"].toString(""); - if (!pack.slug.isEmpty()) + if (!pack.slug.isEmpty()) { pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; - else + } else { pack.websiteUrl = ""; + } pack.description = obj["description"].toString(""); @@ -57,7 +57,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) if (obj.contains("author")) { ModPlatform::ModpackAuthor modAuthor; modAuthor.name = obj["author"].toString(); - modAuthor.url = api.getAuthorURL(modAuthor.name); + modAuthor.url = ModrinthAPI::getAuthorURL(modAuthor.name); pack.authors = { modAuthor }; } @@ -91,8 +91,9 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraData.wikiUrl.chop(1); pack.extraData.discordUrl = obj["discord_url"].toString(); - if (pack.extraData.discordUrl.endsWith('/')) + if (pack.extraData.discordUrl.endsWith('/')) { pack.extraData.discordUrl.chop(1); + } auto donate_arr = obj["donation_urls"].toArray(); for (auto d : donate_arr) { @@ -114,7 +115,9 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraDataLoaded = true; } -ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) +ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, + const QString& preferred_hash_type, + const QString& preferred_file_name) { ModPlatform::IndexedVersion file; @@ -131,24 +134,27 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { - if (loader == "neoforge") + if (loader == "neoforge") { file.loaders |= ModPlatform::NeoForge; - else if (loader == "forge") + } else if (loader == "forge") { file.loaders |= ModPlatform::Forge; - else if (loader == "cauldron") + } else if (loader == "cauldron") { file.loaders |= ModPlatform::Cauldron; - else if (loader == "liteloader") + } else if (loader == "liteloader") { file.loaders |= ModPlatform::LiteLoader; - else if (loader == "fabric") + } else if (loader == "fabric") { file.loaders |= ModPlatform::Fabric; - else if (loader == "quilt") + } else if (loader == "quilt") { file.loaders |= ModPlatform::Quilt; + } } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); file.version_type = ModPlatform::IndexedVersionType::fromString(Json::requireString(obj, "version_type")); - file.changelog = Json::requireString(obj, "changelog"); + if (obj.contains("changelog")) { + file.changelog = Json::requireString(obj, "changelog"); + } auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { @@ -158,16 +164,17 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q dependency.version = dep["version_id"].toString(); auto depType = Json::requireString(dep, "dependency_type"); - if (depType == "required") + if (depType == "required") { dependency.type = ModPlatform::DependencyType::REQUIRED; - else if (depType == "optional") + } else if (depType == "optional") { dependency.type = ModPlatform::DependencyType::OPTIONAL; - else if (depType == "incompatible") + } else if (depType == "incompatible") { dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; - else if (depType == "embedded") + } else if (depType == "embedded") { dependency.type = ModPlatform::DependencyType::EMBEDDED; - else + } else { dependency.type = ModPlatform::DependencyType::UNKNOWN; + } file.dependencies.append(dependency); } @@ -195,8 +202,9 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q } // Grab the primary file, if available - if (Json::requireBoolean(parent, "primary")) + if (Json::requireBoolean(parent, "primary")) { break; + } i++; } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 5d852cb6f..b38cd9ce6 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -23,8 +23,9 @@ namespace Modrinth { -void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; +void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj); +void loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj); +auto loadIndexedPackVersion(QJsonObject& obj, const QString& preferred_hash_type = "sha512", const QString& preferred_file_name = "") + -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index a9d75b4ae..4d162a90d 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -22,73 +22,89 @@ #include #include #include +#include +#include #include #include +#include #include "FileSystem.h" #include "StringUtils.h" +#include "Version.h" #include "modplatform/ModIndex.h" #include namespace Packwiz { -auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +namespace { +auto getRealIndexName(const QDir& indexDir, const QString& normalizedFname, bool shouldFindMatch = false) -> QString { - QFile index_file(index_dir.absoluteFilePath(normalized_fname)); + const QFile indexFile(indexDir.absoluteFilePath(normalizedFname)); - QString real_fname = normalized_fname; - if (!index_file.exists()) { + QString realFname = normalizedFname; + if (!indexFile.exists()) { // Tries to get similar entries - for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { - if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) { - real_fname = file_name; + for (auto& fileName : indexDir.entryList(QDir::Filter::Files)) { + if (QString::compare(normalizedFname, fileName, Qt::CaseInsensitive) == 0) { + realFname = fileName; break; } } - if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)) { + if (shouldFindMatch && (QString::compare(normalizedFname, realFname, Qt::CaseSensitive) == 0)) { qCritical() << "Could not find a match for a valid metadata file!"; - qCritical() << "File:" << normalized_fname; + qCritical() << "File:" << normalizedFname; return {}; } } - return real_fname; + return realFname; } // Helpers -static inline auto indexFileName(QString const& mod_slug) -> QString +auto indexFileName(const QString& modSlug) -> QString { - if (mod_slug.endsWith(".pw.toml")) - return mod_slug; - return QString("%1.pw.toml").arg(mod_slug); + if (modSlug.endsWith(".pw.toml")) { + return modSlug; + } + return QString("%1.pw.toml").arg(modSlug); } // Helper functions for extracting data from the TOML file -auto stringEntry(toml::table table, QString entry_name) -> QString +auto stringEntry(toml::table table, const QString& entryName) -> QString { - auto node = table[StringUtils::toStdString(entry_name)]; + auto* node = table.get(StringUtils::toStdString(entryName)); if (!node) { - qDebug() << "Failed to read str property '" + entry_name + "' in mod metadata."; + qDebug() << "Failed to read str property '" + entryName + "' in mod metadata."; return {}; } - return node.value_or(""); + return node->value_or(""); } -auto intEntry(toml::table table, QString entry_name) -> int +auto intEntry(toml::table table, const QString& entryName) -> int { - auto node = table[StringUtils::toStdString(entry_name)]; + auto* node = table.get(StringUtils::toStdString(entryName)); if (!node) { - qDebug() << "Failed to read int property '" + entry_name + "' in mod metadata."; + qDebug() << "Failed to read int property '" + entryName + "' in mod metadata."; return {}; } - return node.value_or(0); + return node->value_or(0); } +bool sortMCVersions(const QString& a, const QString& b) +{ + auto cmp = Version(a) <=> Version(b); + if (cmp == std::strong_ordering::equal) { + return a < b; + } + return cmp == std::strong_ordering::less; +} + +} // namespace auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod @@ -115,13 +131,15 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; mod.loaders = mod_version.loaders; mod.mcVersions = mod_version.mcVersion; - mod.mcVersions.sort(); + mod.mcVersions.removeDuplicates(); + std::ranges::sort(mod.mcVersions, sortMCVersions); mod.releaseType = mod_version.version_type; mod.version_number = mod_version.version_number; if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; + mod.dependencies = mod_version.dependencies; return mod; } @@ -186,10 +204,20 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) } if (!index_file.open(QIODevice::ReadWrite)) { - qCritical() << QString("Could not open file %1!").arg(normalized_fname); + qCritical() << "Could not open file" << normalized_fname << "error:" << index_file.errorString(); return; } + toml::array deps; + for (auto dep : mod.dependencies) { + auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() }, + { "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } }; + if (!dep.version.isEmpty()) { + tbl.emplace("version", dep.version.toStdString()); + } + deps.push_back(tbl); + } + // Put TOML data into the file QTextStream in_stream(&index_file); { @@ -200,6 +228,7 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-version-number", mod.version_number.toStdString() }, + { "x-prismlauncher-dependencies", deps }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -289,7 +318,8 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod } } } - mod.mcVersions.sort(); + mod.mcVersions.removeDuplicates(); + std::ranges::sort(mod.mcVersions, sortMCVersions); } } mod.version_number = table["x-prismlauncher-version-number"].value_or(""); @@ -330,6 +360,23 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod return {}; } } + { // dependencies + auto deps = table["x-prismlauncher-dependencies"].as_array(); + if (deps) { + for (auto&& depNode : *deps) { + auto dep = depNode.as_table(); + if (dep) { + ModPlatform::Dependency d; + d.addonId = stringEntry(*dep, "addonId"); + if (dep->contains("version")) { + d.version = stringEntry(*dep, "version"); + } + d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type")); + mod.dependencies << d; + } + } + } + } return mod; } diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index ba9a0fe75..b5b817755 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -29,8 +29,6 @@ class QDir; namespace Packwiz { -auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; - class V1 { public: // can also represent other resources beside loader mods - but this is what packwiz calls it @@ -55,6 +53,8 @@ class V1 { QVariant project_id{}; QString version_number{}; + QList dependencies; + public: // This is a totally heuristic, but should work for now. auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index 852f27241..e7f4b7a55 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -45,7 +45,7 @@ #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" -Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, +Technic::SolderPackInstallTask::SolderPackInstallTask(QNetworkAccessManager* network, const QUrl& solderUrl, const QString& pack, const QString& version, @@ -72,24 +72,25 @@ void Technic::SolderPackInstallTask::executeTask() m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); - m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response)); + auto [action, response] = Net::ApiDownload::makeByteArray(sourceUrl); + m_filesNetJob->addNetAction(action); auto job = m_filesNetJob.get(); - connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); + connect(job, &NetJob::succeeded, this, [this, response] { fileListSucceeded(response); }); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } -void Technic::SolderPackInstallTask::fileListSucceeded() +void Technic::SolderPackInstallTask::fileListSucceeded(QByteArray* response) { setStatus(tr("Downloading modpack")); QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *m_response; + qWarning() << *response; return; } auto obj = doc.object(); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 2ea701e23..07cce4644 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -40,13 +40,12 @@ #include #include -#include namespace Technic { class SolderPackInstallTask : public InstanceTask { Q_OBJECT public: - explicit SolderPackInstallTask(shared_qobject_ptr network, + explicit SolderPackInstallTask(QNetworkAccessManager* network, const QUrl& solderUrl, const QString& pack, const QString& version, @@ -60,7 +59,7 @@ class SolderPackInstallTask : public InstanceTask { virtual void executeTask() override; private slots: - void fileListSucceeded(); + void fileListSucceeded(QByteArray* response); void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); @@ -71,14 +70,13 @@ class SolderPackInstallTask : public InstanceTask { private: bool m_abortable = false; - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; NetJob::Ptr m_filesNetJob; QUrl m_solderUrl; QString m_pack; QString m_version; QString m_minecraftVersion; - std::shared_ptr m_response = std::make_shared(); QTemporaryDir m_outputDir; int m_modCount; QFuture m_extractFuture; diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 4c40ddf73..858f4ae6b 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -24,7 +24,7 @@ #include #include "archive/ArchiveReader.h" -void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, +void Technic::TechnicPackProcessor::run(SettingsObject* globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, @@ -33,8 +33,8 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, { QString minecraftPath = FS::PathCombine(stagingPath, "minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); - auto instanceSettings = std::make_shared(configPath); - MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); + auto instanceSettings = std::make_unique(configPath); + MinecraftInstance instance(globalSettings, std::move(instanceSettings), stagingPath); instance.setName(instName); @@ -117,7 +117,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, } else if (QFile::exists(versionJson)) { QFile file(versionJson); if (!file.open(QIODevice::ReadOnly)) { - emit failed(tr("Unable to open \"version.json\"!")); + emit failed(tr("Unable to open \"version.json\": %1").arg(file.errorString())); return; } data = file.readAll(); diff --git a/launcher/modplatform/technic/TechnicPackProcessor.h b/launcher/modplatform/technic/TechnicPackProcessor.h index 08e117fd8..0d2dabc93 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.h +++ b/launcher/modplatform/technic/TechnicPackProcessor.h @@ -28,7 +28,7 @@ class TechnicPackProcessor : public QObject { void failed(QString reason); public: - void run(SettingsObjectPtr globalSettings, + void run(SettingsObject* globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, diff --git a/launcher/net/ApiDownload.cpp b/launcher/net/ApiDownload.cpp index 78eb1f851..54d3e746e 100644 --- a/launcher/net/ApiDownload.cpp +++ b/launcher/net/ApiDownload.cpp @@ -18,28 +18,30 @@ */ #include "net/ApiDownload.h" + +#include #include "net/ApiHeaderProxy.h" namespace Net { Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) { - auto dl = Download::makeCached(url, entry, options); - dl->addHeaderProxy(new ApiHeaderProxy()); + auto dl = Download::makeCached(std::move(url), std::move(entry), options); + dl->addHeaderProxy(std::make_unique()); return dl; } -Download::Ptr ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Download::Options options) +std::pair ApiDownload::makeByteArray(QUrl url, Download::Options options) { - auto dl = Download::makeByteArray(url, output, options); - dl->addHeaderProxy(new ApiHeaderProxy()); - return dl; + auto [dl, response] = Download::makeByteArray(std::move(url), options); + dl->addHeaderProxy(std::make_unique()); + return { dl, response }; } -Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) +Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options, ModrinthDownloadMeta meta) { - auto dl = Download::makeFile(url, path, options); - dl->addHeaderProxy(new ApiHeaderProxy()); + auto dl = Download::makeFile(std::move(url), std::move(path), options); + dl->addHeaderProxy(std::make_unique(std::move(meta))); return dl; } diff --git a/launcher/net/ApiDownload.h b/launcher/net/ApiDownload.h index 842c25c56..033dd0e12 100644 --- a/launcher/net/ApiDownload.h +++ b/launcher/net/ApiDownload.h @@ -20,13 +20,17 @@ #pragma once #include "Download.h" +#include "net/ApiHeaderProxy.h" namespace Net { namespace ApiDownload { Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); -Download::Ptr makeByteArray(QUrl url, std::shared_ptr output, Download::Options options = Download::Option::NoOptions); -Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); +std::pair makeByteArray(QUrl url, Download::Options options = Download::Option::NoOptions); +Download::Ptr makeFile(QUrl url, + QString path, + Download::Options options = Download::Option::NoOptions, + ModrinthDownloadMeta meta = ModrinthDownloadMeta()); }; // namespace ApiDownload } // namespace Net diff --git a/launcher/net/ApiHeaderProxy.h b/launcher/net/ApiHeaderProxy.h index 789a6fada..d12287a4e 100644 --- a/launcher/net/ApiHeaderProxy.h +++ b/launcher/net/ApiHeaderProxy.h @@ -23,27 +23,64 @@ #include "BuildConfig.h" #include "net/HeaderProxy.h" +#include +#include + namespace Net { +struct ModrinthDownloadMeta { + QString reason; + QString gameVersion; + QString loader; + + bool isEmpty() const { return reason.isEmpty(); } + + QByteArray toJson() const + { + QJsonObject obj; + if (!reason.isEmpty()) { + obj["reason"] = reason; + } + if (!gameVersion.isEmpty()) { + obj["game_version"] = gameVersion; + } + if (!loader.isEmpty()) { + obj["loader"] = loader; + } + return QJsonDocument(obj).toJson(QJsonDocument::Compact); + } +}; + class ApiHeaderProxy : public HeaderProxy { public: - ApiHeaderProxy() : HeaderProxy() {} - virtual ~ApiHeaderProxy() = default; + ApiHeaderProxy() = default; + explicit ApiHeaderProxy(ModrinthDownloadMeta meta) : m_meta(std::move(meta)) {} + ~ApiHeaderProxy() override = default; public: - virtual QList headers(const QNetworkRequest& request) const override + QList headers(const QNetworkRequest& request) const override { QList hdrs; - if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { - hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() }); - } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || - request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { + const auto host = request.url().host(); + + if (APPLICATION->capabilities() & Application::SupportsFlame && + (host == QUrl(BuildConfig.FLAME_BASE_URL).host() || host == BuildConfig.FLAME_DOWNLOAD_HOST)) { + hdrs.append({ .headerName = "x-api-key", .headerValue = APPLICATION->getFlameAPIKey().toUtf8() }); + } else if (host == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || host == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { QString token = APPLICATION->getModrinthAPIToken(); - if (!token.isNull()) - hdrs.append({ "Authorization", token.toUtf8() }); + if (!token.isNull()) { + hdrs.append({ .headerName = "Authorization", .headerValue = token.toUtf8() }); + } + } + + if (host == BuildConfig.MODRINTH_DOWNLOAD_HOST && !m_meta.isEmpty()) { + hdrs.append({ .headerName = "modrinth-download-meta", .headerValue = m_meta.toJson() }); } return hdrs; }; + + private: + ModrinthDownloadMeta m_meta; }; } // namespace Net diff --git a/launcher/net/ApiUpload.cpp b/launcher/net/ApiUpload.cpp index a2b8f357b..261558130 100644 --- a/launcher/net/ApiUpload.cpp +++ b/launcher/net/ApiUpload.cpp @@ -22,11 +22,11 @@ namespace Net { -Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +std::pair ApiUpload::makeByteArray(QUrl url, QByteArray m_post_data) { - auto up = Upload::makeByteArray(url, output, m_post_data); - up->addHeaderProxy(new ApiHeaderProxy()); - return up; + auto [up, response] = Upload::makeByteArray(url, m_post_data); + up->addHeaderProxy(std::make_unique()); + return { up, response }; } } // namespace Net diff --git a/launcher/net/ApiUpload.h b/launcher/net/ApiUpload.h index 674a3b93f..3aa4adeab 100644 --- a/launcher/net/ApiUpload.h +++ b/launcher/net/ApiUpload.h @@ -24,7 +24,7 @@ namespace Net { namespace ApiUpload { -Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); +std::pair makeByteArray(QUrl url, QByteArray m_post_data); }; } // namespace Net diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index f68230838..b03d7192a 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -41,21 +41,16 @@ namespace Net { /* - * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + * Sink object for downloads that uses an owned QByteArray as a target. */ class ByteArraySink : public Sink { public: - ByteArraySink(std::shared_ptr output) : m_output(output) {}; - virtual ~ByteArraySink() = default; public: auto init(QNetworkRequest& request) -> Task::State override { - if (m_output) - m_output->clear(); - else - qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; + m_output.clear(); if (initAllValidators(request)) return Task::State::Running; m_fail_reason = "Failed to initialize validators"; @@ -64,10 +59,7 @@ class ByteArraySink : public Sink { auto write(QByteArray& data) -> Task::State override { - if (m_output) - m_output->append(data); - else - qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; + m_output.append(data); if (writeAllValidators(data)) return Task::State::Running; m_fail_reason = "Failed to write validators"; @@ -91,7 +83,9 @@ class ByteArraySink : public Sink { auto hasLocalData() -> bool override { return false; } + QByteArray* output() { return &m_output; } + protected: - std::shared_ptr m_output; + QByteArray m_output; }; } // namespace Net diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 7663d5d12..c7906cc13 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -38,7 +38,6 @@ #include "Validator.h" #include -#include namespace Net { class ChecksumValidator : public Validator { @@ -69,10 +68,10 @@ class ChecksumValidator : public Validator { return true; } - auto validate(QNetworkReply&) -> bool override + auto validate(QNetworkReply& reply) -> bool override { - if (m_expected.size() && m_expected != hash()) { - qWarning() << "Checksum mismatch, download is bad."; + if (!m_expected.isEmpty() && m_expected != hash()) { + qWarning() << "Checksum mismatch for URL:" << reply.url().toString() << "expected:" << m_expected << "got:" << hash(); return false; } return true; diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 49686db98..9a22c87e8 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -63,14 +63,18 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down } #endif -auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr +auto Download::makeByteArray(QUrl url, Options options) -> std::pair { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("BYTES:") + url.toString()); dl->m_options = options; - dl->m_sink.reset(new ByteArraySink(output)); - return dl; + + auto sink = std::make_unique(); + QByteArray* response = sink->output(); + dl->m_sink = std::move(sink); + + return { dl, response }; } auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 5f6a5caf1..60a5b5b64 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -38,12 +38,16 @@ #pragma once +#include + #include "HttpMetaCache.h" #include "QObjectPtr.h" #include "net/NetRequest.h" namespace Net { +class ByteArraySink; + class Download : public NetRequest { Q_OBJECT public: @@ -54,7 +58,11 @@ class Download : public NetRequest { static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; #endif - static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; + /** + * Creates a request downloading to the returned QByteArray,. + * The QByteArray will live as long as the Download object. + */ + static auto makeByteArray(QUrl url, Options options = Option::NoOptions) -> std::pair; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; protected: diff --git a/launcher/net/DummySink.h b/launcher/net/DummySink.h new file mode 100644 index 000000000..fa540fd2d --- /dev/null +++ b/launcher/net/DummySink.h @@ -0,0 +1,34 @@ +// 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 + +namespace Net { + +class DummySink : public Sink { + public: + explicit DummySink() {} + ~DummySink() override {} + auto init(QNetworkRequest& request) -> Task::State override { return Task::State::Running; } + auto write(QByteArray& data) -> Task::State override { return Task::State::Succeeded; } + auto abort() -> Task::State override { return Task::State::AbortedByUser; } + auto finalize(QNetworkReply& reply) -> Task::State override { return Task::State::Succeeded; } + auto hasLocalData() -> bool override { return false; } +}; + +} // namespace Net diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index f61dd527d..47838f62c 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -58,8 +58,9 @@ Task::State FileSink::init(QNetworkRequest& request) m_wroteAnyData = false; m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { - qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; - m_fail_reason = "Could not open file"; + const auto error = QString("Could not open %1 for writing: %2").arg(m_filename).arg(m_output_file->errorString()); + qCCritical(taskNetLogC) << error; + m_fail_reason = error; return Task::State::Failed; } @@ -72,11 +73,17 @@ Task::State FileSink::init(QNetworkRequest& request) Task::State FileSink::write(QByteArray& data) { if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { - qCCritical(taskNetLogC) << "Failed writing into " + m_filename; + QString error = QString("Failed writing into %1: %2").arg(m_filename); + if (m_output_file->error() == QFileDevice::NoError) { + error = error.arg("Validators failed"); + } else { + error = error.arg(m_output_file->errorString()); + } + qCCritical(taskNetLogC) << error; + m_fail_reason = error; m_output_file->cancelWriting(); m_output_file.reset(); m_wroteAnyData = false; - m_fail_reason = "Failed to write validators"; return Task::State::Failed; } @@ -116,9 +123,10 @@ Task::State FileSink::finalize(QNetworkReply& reply) // nothing went wrong... if (!m_output_file->commit()) { - qCCritical(taskNetLogC) << "Failed to commit changes to" << m_filename; + const auto error = QString("Failed to commit changes to %1: %2").arg(m_filename).arg(m_output_file->errorString()); + qCCritical(taskNetLogC) << error; + m_fail_reason = error; m_output_file->cancelWriting(); - m_fail_reason = "Failed to commit changes"; return Task::State::Failed; } } diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 9ad9fdf43..5c1e47dfd 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -113,7 +113,7 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); if (!input.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file '" << input.fileName() << "' for reading!"; + qWarning() << "Failed to open file" << input.fileName() << "for reading:" << input.errorString(); return staleEntry(base, resource_path); } QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); @@ -182,7 +182,7 @@ auto HttpMetaCache::evictAll() -> bool } map.entry_list.clear(); // AND all return codes together so the result is true iff all runs of deletePath() are true - ret &= FS::deletePath(map.base_path); + ret &= FS::deleteContents(map.base_path); } return ret; } diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 335e360b2..3dd1c09cf 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -41,11 +41,11 @@ #include "tasks/ConcurrentTask.h" #if defined(LAUNCHER_APPLICATION) #include "Application.h" -#include "ui/dialogs/CustomMessageBox.h" +#include "settings/SettingsObject.h" +#include "ui/dialogs/NetworkJobFailedDialog.h" #endif -NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) - : ConcurrentTask(job_name), m_network(network) +NetJob::NetJob(QString job_name, QNetworkAccessManager* network, int max_concurrent) : ConcurrentTask(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && max_concurrent < 0) @@ -69,11 +69,15 @@ void NetJob::executeNextSubTask() // We're finished, check for failures and retry if we can (up to 3 times) if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { m_try += 1; - while (!m_failed.isEmpty()) { - auto task = m_failed.take(*m_failed.keyBegin()); - m_done.remove(task.get()); - m_queue.enqueue(task); - } + m_failed.removeIf([this](QHash::iterator task) { + // there is no point in retying on 404 Not Found + if (static_cast(task->get())->replyStatusCode() == 404) { + return false; + } + m_done.remove(task->get()); + m_queue.enqueue(*task); + return true; + }); } ConcurrentTask::executeNextSubTask(); } @@ -100,13 +104,18 @@ auto NetJob::canAbort() const -> bool auto NetJob::abort() -> bool { - bool fullyAborted = true; - // fail all downloads on the queue for (auto task : m_queue) m_failed.insert(task.get(), task); m_queue.clear(); + if (m_doing.isEmpty()) { + // no downloads to abort, NetJob is not running + return true; + } + + bool fullyAborted = true; + // abort active downloads auto toKill = m_doing.values(); for (auto part : toKill) { @@ -164,23 +173,29 @@ void NetJob::emitFailed(QString reason) if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { m_manual_try++; - auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", - "The tasks failed.\n" - "Failed urls\n" + - getFailedFiles().join("\n\t") + - ".\n" - "If this continues to happen please check the logs of the application.\n" - "Do you want to retry?", - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); + auto failed = getFailedActions(); + auto dialog = new NetworkJobFailedDialog(objectName(), m_try, m_done.size(), failed.size(), nullptr); + dialog->setAttribute(Qt::WA_DeleteOnClose); - if (response == QMessageBox::Yes) { - m_try = 0; - executeNextSubTask(); - return; + for (const auto& request : failed) { + dialog->addFailedRequest(request->url(), request->errorString()); } + + dialog->open(); + + connect(dialog, &QDialog::finished, this, [this, reason = std::move(reason)](int result) { + if (result == QDialog::Accepted) { + m_try = 0; + executeNextSubTask(); + } else { + ConcurrentTask::emitFailed(reason); + } + }); + + return; } #endif + ConcurrentTask::emitFailed(reason); } diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index 59213ba15..e8351f686 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -50,9 +50,10 @@ class NetJob : public ConcurrentTask { Q_OBJECT public: + // TODO: delete using Ptr = shared_qobject_ptr; - explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = -1); + explicit NetJob(QString job_name, QNetworkAccessManager* network, int max_concurrent = -1); ~NetJob() override = default; auto size() const -> int; @@ -77,7 +78,7 @@ class NetJob : public ConcurrentTask { bool isOnline(); private: - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; int m_try = 1; bool m_ask_retry = true; diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index da909e65b..7faeffbe0 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -49,6 +49,7 @@ #if defined(LAUNCHER_APPLICATION) #include "Application.h" +#include "settings/SettingsObject.h" #endif #include "BuildConfig.h" diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index 3b3c90774..e38152b83 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -69,8 +69,8 @@ class NetRequest : public Task { auto abort() -> bool override; auto canAbort() const -> bool override { return true; } - void setNetwork(shared_qobject_ptr network) { m_network = network; } - void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr(proxy)); } + void setNetwork(QNetworkAccessManager* network) { m_network = network; } + void addHeaderProxy(std::unique_ptr proxy) { m_headerProxies.push_back(std::move(proxy)); } // automatically handle HTTP 429 Too Many Requests errors and retry void enableAutoRetry(bool enable); @@ -105,15 +105,15 @@ class NetRequest : public Task { std::chrono::time_point m_last_progress_time; qint64 m_last_progress_bytes; - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; /// the network reply - unique_qobject_ptr m_reply; + std::unique_ptr m_reply; QByteArray m_errorResponse; /// source URL QUrl m_url; - std::vector> m_headerProxies; + std::vector> m_headerProxies; int m_retryCount = 0; QTimer m_retryTimer; diff --git a/launcher/net/NetUtils.h b/launcher/net/NetUtils.h index cd517bcca..67ebc9685 100644 --- a/launcher/net/NetUtils.h +++ b/launcher/net/NetUtils.h @@ -40,4 +40,18 @@ inline bool isApplicationError(QNetworkReply::NetworkError x) QNetworkReply::UnknownContentError }; return errors.contains(x); } + +// 500 class errors, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500 +// microsoft may send these error codes when services (auth) are down. +// We treat this as a reason to launch in offline mode. +inline bool isServerError(QNetworkReply::NetworkError x) +{ + static QSet errors = { QNetworkReply::InternalServerError, + QNetworkReply::OperationNotImplementedError, + QNetworkReply::ServiceUnavailableError, // 503 | seen in logs in 2026 + //QNetworkReply::GatewayTimeoutError, // 504 | seen in logs in 2024 + // Qt doesn't have it mapped. Unknown covers it + QNetworkReply::UnknownServerError }; + return errors.contains(x); +} } // namespace Net diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index a72d63f29..ecbf4e201 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -119,11 +119,11 @@ auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State switch (m_d->m_paste_type) { case PasteUpload::NullPointer: - m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed(); + m_d->m_pasteLink = QString::fromUtf8(*output()).trimmed(); break; case PasteUpload::Hastebin: { QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); m_fail_reason = @@ -144,7 +144,7 @@ auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State } case PasteUpload::Mclogs: { QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); m_fail_reason = @@ -171,7 +171,7 @@ auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State } case PasteUpload::PasteGG: QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*output(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); m_fail_reason = diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 7f43779c4..d22a9ba47 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -45,6 +45,7 @@ #include #include +#include class PasteUpload : public Net::NetRequest { public: @@ -71,7 +72,7 @@ class PasteUpload : public Net::NetRequest { class Sink : public Net::ByteArraySink { public: - Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared()), m_d(p) {}; + Sink(PasteUpload* p) : m_d(p) {}; virtual ~Sink() = default; public: diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 623ec80f4..60cf6d3ec 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -51,12 +51,16 @@ QNetworkReply* Upload::getReply(QNetworkRequest& request) return m_network->post(request, m_post_data); } -Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +std::pair Upload::makeByteArray(QUrl url, QByteArray m_post_data) { auto up = makeShared(); up->m_url = std::move(url); - up->m_sink.reset(new ByteArraySink(output)); + + auto sink = std::make_unique(); + QByteArray* response = sink->output(); + up->m_sink = std::move(sink); + up->m_post_data = std::move(m_post_data); - return up; + return { up, response }; } } // namespace Net diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index f920e5561..0610426a1 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -37,6 +37,8 @@ #pragma once +#include + #include "net/NetRequest.h" namespace Net { @@ -47,7 +49,11 @@ class Upload : public NetRequest { using Ptr = shared_qobject_ptr; explicit Upload() : NetRequest() { logCat = taskUploadLogC; }; - static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + /** + * Creates a request downloading to the returned QByteArray,. + * The QByteArray will live as long as the Upload object. + */ + static std::pair makeByteArray(QUrl url, QByteArray m_post_data); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index dc4447aba..35173dd7b 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -39,8 +39,9 @@ #include #include +#include "Application.h" -NewsChecker::NewsChecker(shared_qobject_ptr network, const QString& feedUrl) +NewsChecker::NewsChecker(QNetworkAccessManager* network, const QString& feedUrl) { m_network = network; m_feedUrl = feedUrl; @@ -54,10 +55,12 @@ void NewsChecker::reloadNews() return; } + m_entry = APPLICATION->metacache()->resolveEntry("feed", "feed.xml"); + qDebug() << "Reloading news."; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; - job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); + job->addNetAction(Net::Download::makeCached(m_feedUrl, m_entry)); job->setAskRetry(false); connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); @@ -78,14 +81,16 @@ void NewsChecker::rssDownloadFinished() int errorLine = -1; int errorCol = -1; - // Parse the XML. - if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) { - QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); - fail(fullErrorMsg); - newsData->clear(); - return; + QFile feed(m_entry->getFullPath()); + + if (feed.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&feed); + // Parse the XML. + if (!doc.setContent(in.readAll(), false, &errorMsg, &errorLine, &errorCol)) { + fail(QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol)); + return; + } } - newsData->clear(); } // If the parsing succeeded, read it. diff --git a/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h index cdd621a20..497ae2319 100644 --- a/launcher/news/NewsChecker.h +++ b/launcher/news/NewsChecker.h @@ -29,7 +29,7 @@ class NewsChecker : public QObject { /*! * Constructs a news reader to read from the given RSS feed URL. */ - NewsChecker(shared_qobject_ptr network, const QString& feedUrl); + NewsChecker(QNetworkAccessManager* network, const QString& feedUrl); /*! * Returns the error message for the last time the news was loaded. @@ -84,7 +84,8 @@ class NewsChecker : public QObject { //! True if news has been loaded. bool m_loadedNews; - std::shared_ptr newsData = std::make_shared(); + //! The cache entry for the feed. + MetaEntryPtr m_entry; /*! * Gets the error message that was given last time the news was loaded. @@ -92,7 +93,7 @@ class NewsChecker : public QObject { */ QString m_lastLoadError; - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; protected slots: /// Emits newsLoaded() and sets m_lastLoadError to empty string. diff --git a/launcher/resources/multimc/16x16/about.png b/launcher/resources/multimc/16x16/about.png deleted file mode 100644 index ed7e56dd6..000000000 Binary files a/launcher/resources/multimc/16x16/about.png and /dev/null differ diff --git a/launcher/resources/multimc/16x16/checkupdate.png b/launcher/resources/multimc/16x16/checkupdate.png deleted file mode 100644 index 9d08c56f0..000000000 Binary files a/launcher/resources/multimc/16x16/checkupdate.png and /dev/null differ diff --git a/launcher/resources/multimc/16x16/new.png b/launcher/resources/multimc/16x16/new.png deleted file mode 100644 index dfde06f61..000000000 Binary files a/launcher/resources/multimc/16x16/new.png and /dev/null differ diff --git a/launcher/resources/multimc/22x22/about.png b/launcher/resources/multimc/22x22/about.png deleted file mode 100644 index fbf18726f..000000000 Binary files a/launcher/resources/multimc/22x22/about.png and /dev/null differ diff --git a/launcher/resources/multimc/22x22/checkupdate.png b/launcher/resources/multimc/22x22/checkupdate.png deleted file mode 100644 index a44d47fe0..000000000 Binary files a/launcher/resources/multimc/22x22/checkupdate.png and /dev/null differ diff --git a/launcher/resources/multimc/22x22/new.png b/launcher/resources/multimc/22x22/new.png deleted file mode 100644 index 41edf3ef0..000000000 Binary files a/launcher/resources/multimc/22x22/new.png and /dev/null differ diff --git a/launcher/resources/multimc/32x32/about.png b/launcher/resources/multimc/32x32/about.png deleted file mode 100644 index 261d2a44c..000000000 Binary files a/launcher/resources/multimc/32x32/about.png and /dev/null differ diff --git a/launcher/resources/multimc/32x32/checkupdate.png b/launcher/resources/multimc/32x32/checkupdate.png deleted file mode 100644 index c60f965b2..000000000 Binary files a/launcher/resources/multimc/32x32/checkupdate.png and /dev/null differ diff --git a/launcher/resources/multimc/32x32/new.png b/launcher/resources/multimc/32x32/new.png deleted file mode 100644 index 8fc4be64a..000000000 Binary files a/launcher/resources/multimc/32x32/new.png and /dev/null differ diff --git a/launcher/resources/multimc/48x48/about.png b/launcher/resources/multimc/48x48/about.png deleted file mode 100644 index f6d4d11cb..000000000 Binary files a/launcher/resources/multimc/48x48/about.png and /dev/null differ diff --git a/launcher/resources/multimc/48x48/checkupdate.png b/launcher/resources/multimc/48x48/checkupdate.png deleted file mode 100644 index b181736a8..000000000 Binary files a/launcher/resources/multimc/48x48/checkupdate.png and /dev/null differ diff --git a/launcher/resources/multimc/48x48/new.png b/launcher/resources/multimc/48x48/new.png deleted file mode 100644 index a81ccf1b2..000000000 Binary files a/launcher/resources/multimc/48x48/new.png and /dev/null differ diff --git a/launcher/resources/multimc/64x64/about.png b/launcher/resources/multimc/64x64/about.png deleted file mode 100644 index b9be9abef..000000000 Binary files a/launcher/resources/multimc/64x64/about.png and /dev/null differ diff --git a/launcher/resources/multimc/64x64/checkupdate.png b/launcher/resources/multimc/64x64/checkupdate.png deleted file mode 100644 index a4002a61e..000000000 Binary files a/launcher/resources/multimc/64x64/checkupdate.png and /dev/null differ diff --git a/launcher/resources/multimc/64x64/new.png b/launcher/resources/multimc/64x64/new.png deleted file mode 100644 index 289a6ad0b..000000000 Binary files a/launcher/resources/multimc/64x64/new.png and /dev/null differ diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 2937cc34b..2a4736a93 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -49,13 +49,6 @@ 48x48/minecraft.png 256x256/minecraft.png - - 16x16/about.png - 22x22/about.png - 32x32/about.png - 48x48/about.png - 64x64/about.png - scalable/bug.svg 16x16/bug.png @@ -92,14 +85,6 @@ 48x48/centralmods.png 64x64/centralmods.png - - scalable/checkupdate.svg - 16x16/checkupdate.png - 22x22/checkupdate.png - 32x32/checkupdate.png - 48x48/checkupdate.png - 64x64/checkupdate.png - 16x16/copy.png 22x22/copy.png @@ -114,13 +99,6 @@ 48x48/help.png 64x64/help.png - - 16x16/new.png - 22x22/new.png - 32x32/new.png - 48x48/new.png - 64x64/new.png - scalable/news.svg 16x16/news.png @@ -337,16 +315,18 @@ scalable/instances/fox_legacy.svg scalable/instances/bee_legacy.svg - + scalable/delete.svg scalable/tag.svg scalable/rename.svg scalable/shortcut.svg - scalable/export.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg + scalable/about.svg + scalable/new.svg + scalable/checkupdate.svg scalable/instances/quiltmc.svg scalable/instances/fabricmc.svg @@ -358,6 +338,7 @@ scalable/adoptium.svg scalable/azul.svg scalable/mojang.svg + scalable/openj9_hex_custom.svg diff --git a/launcher/resources/multimc/scalable/about.svg b/launcher/resources/multimc/scalable/about.svg new file mode 100644 index 000000000..b97c79d89 --- /dev/null +++ b/launcher/resources/multimc/scalable/about.svg @@ -0,0 +1,3928 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xmlimage/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/resources/multimc/scalable/about.svg.license b/launcher/resources/multimc/scalable/about.svg.license new file mode 100644 index 000000000..92494ca5c --- /dev/null +++ b/launcher/resources/multimc/scalable/about.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/launcher/resources/multimc/scalable/checkupdate.svg b/launcher/resources/multimc/scalable/checkupdate.svg index fc09cb4c7..4ab18c929 100644 --- a/launcher/resources/multimc/scalable/checkupdate.svg +++ b/launcher/resources/multimc/scalable/checkupdate.svg @@ -1,167 +1,1566 @@ - - - - - - - - - unsorted - - - - - Open Clip Art Library, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml - - - en + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/launcher/resources/multimc/scalable/checkupdate.svg.license b/launcher/resources/multimc/scalable/checkupdate.svg.license new file mode 100644 index 000000000..92494ca5c --- /dev/null +++ b/launcher/resources/multimc/scalable/checkupdate.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/launcher/resources/multimc/scalable/new.svg b/launcher/resources/multimc/scalable/new.svg index c9cff3589..8b9f1a8ab 100644 --- a/launcher/resources/multimc/scalable/new.svg +++ b/launcher/resources/multimc/scalable/new.svg @@ -1,127 +1,602 @@ - - - - - New Document - - - - regular - plaintext - text - document - - - - - Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme - - - - - Jakub Steiner - - - - - Jakub Steiner - - - - image/svg+xml - - - en - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/resources/multimc/scalable/new.svg.license b/launcher/resources/multimc/scalable/new.svg.license new file mode 100644 index 000000000..92494ca5c --- /dev/null +++ b/launcher/resources/multimc/scalable/new.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2007 KDE Community + +SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/launcher/resources/multimc/scalable/openj9_hex_custom.svg b/launcher/resources/multimc/scalable/openj9_hex_custom.svg new file mode 100644 index 000000000..27064b79a --- /dev/null +++ b/launcher/resources/multimc/scalable/openj9_hex_custom.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/openj9_hex_custom.svg.license b/launcher/resources/multimc/scalable/openj9_hex_custom.svg.license new file mode 100644 index 000000000..289f8006f --- /dev/null +++ b/launcher/resources/multimc/scalable/openj9_hex_custom.svg.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2017-2026 Ronald Servant +SPDX-FileCopyrightText: 2026 Ludgie + +SPDX-License-Identifier: Apache-2.0 diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 010beb124..411e4f793 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -54,7 +54,7 @@ Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptrm_url = BuildConfig.IMGUR_BASE_URL + "album"; up->m_sink.reset(new Sink(output)); up->m_screenshots = screenshots; - up->addHeaderProxy(new Net::RawHeaderProxy( + up->addHeaderProxy(std::make_unique( QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 835a1ab81..dd41fc340 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -52,7 +52,7 @@ QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request) auto file = new QFile(m_fileInfo.absoluteFilePath(), this); if (!file->open(QFile::ReadOnly)) { - emitFailed(); + emitFailed(tr("Could not open file %1 for reading: %2").arg(m_fileInfo.absoluteFilePath()).arg(file->errorString())); return nullptr; } @@ -118,9 +118,9 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) { auto up = makeShared(m_shot->m_file); - up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image"); + up->m_url = BuildConfig.IMGUR_BASE_URL + "image"; up->m_sink.reset(new Sink(m_shot)); - up->addHeaderProxy(new Net::RawHeaderProxy(QList{ + up->addHeaderProxy(std::make_unique(QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 75e888938..a1d1c01d9 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -35,6 +35,8 @@ */ #include "settings/INIFile.h" + +#include #include #include @@ -62,11 +64,10 @@ bool INIFile::saveFile(QString fileName) _settings_obj.sync(); if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { - // Shouldn't be possible! - Q_ASSERT(status != QSettings::Status::FormatError); - if (status == QSettings::Status::AccessError) - qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; + qCritical() << "An access error occurred while saving INI file" << fileName << "(is the file read-only?)"; + if (ASSERT_NEVER(status == QSettings::Status::FormatError)) + qCritical() << "A format error occurred while saving INI file" << fileName << "(this shouldn't be possible!)"; return false; } @@ -178,9 +179,9 @@ bool INIFile::loadFile(QString fileName) if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (status == QSettings::Status::AccessError) - qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; + qCritical() << "An access error occurred while loading INI file" << fileName; if (status == QSettings::Status::FormatError) - qCritical() << "A format error occurred (e.g. loading a malformed INI file)."; + qCritical() << "A format error occurred while loading INI file" << fileName << "(is the file malformed or corrupted?)"; return false; } if (!_settings_obj.value("ConfigVersion").isValid()) { diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index abd6c29c5..5743bee54 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -30,9 +30,6 @@ class Setting; class SettingsObject; -using SettingsObjectPtr = std::shared_ptr; -using SettingsObjectWeakPtr = std::weak_ptr; - /*! * \brief The SettingsObject handles communicating settings between the application and a *settings file. @@ -50,11 +47,11 @@ class SettingsObject : public QObject { public: class Lock { public: - Lock(SettingsObjectPtr locked) : m_locked(locked) { m_locked->suspendSave(); } + Lock(SettingsObject* locked) : m_locked(locked) { m_locked->suspendSave(); } ~Lock() { m_locked->resumeSave(); } private: - SettingsObjectPtr m_locked; + SettingsObject* m_locked; }; public: diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index fe5d84bb9..5857a0663 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -48,6 +48,13 @@ Task::Task(bool show_debug) : m_show_debug(show_debug) setAutoDelete(false); } +Task::~Task() +{ + if (isRunning()) { + qCWarning(taskLogC) << "Task" << describe() << "disposed while running!"; + } +} + void Task::setStatus(const QString& new_status) { if (m_status != new_status) { @@ -131,7 +138,7 @@ void Task::emitAborted() return; } m_state = State::AbortedByUser; - m_failReason = "Aborted."; + m_failReason = tr("Aborted"); if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "aborted."; emit aborted(); @@ -194,6 +201,22 @@ QString Task::failReason() const return m_failReason; } +void Task::propagateFromOther(Task* other) +{ + Q_ASSERT(other); + connect(other, &Task::status, this, &Task::setStatus); + connect(other, &Task::details, this, &Task::setDetails); + connect(other, &Task::progress, this, &Task::setProgress); + connect(other, &Task::stepProgress, this, &Task::propagateStepProgress); + + setStatus(other->getStatus()); + setDetails(other->getDetails()); + setProgress(other->getProgress(), other->getTotalProgress()); + for (const auto& progress : other->getStepProgress()) { + propagateStepProgress(*progress); + } +} + void Task::logWarning(const QString& line) { qWarning() << line; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 43e71c8ab..38c09da90 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -94,7 +94,7 @@ class Task : public QObject, public QRunnable { public: explicit Task(bool show_debug_log = true); - virtual ~Task() = default; + ~Task() override; bool isRunning() const; bool isFinished() const; @@ -127,6 +127,9 @@ class Task : public QObject, public QRunnable { QUuid getUid() { return m_uid; } + // Copies the other task's status, details, progress, and step progress to this task; and sets up connections for future propagation + void propagateFromOther(Task* other); + protected: void logWarning(const QString& line); @@ -151,6 +154,8 @@ class Task : public QObject, public QRunnable { //! Emitted when the canAbort() status has changed. */ void abortStatusChanged(bool can_abort); + void abortButtonTextChanged(QString text); + public slots: // QRunnable's interface void run() override { start(); } @@ -160,7 +165,7 @@ class Task : public QObject, public QRunnable { //! used by external code to ask the task to abort virtual bool abort() { - if (canAbort()) + if (canAbort() && isRunning()) emitAborted(); return canAbort(); } @@ -171,6 +176,11 @@ class Task : public QObject, public QRunnable { emit abortStatusChanged(can_abort); } + void setAbortButtonText(QString text) + { + emit abortButtonTextChanged(text); + } + protected: //! The task subclass must implement this method. This method is called to start to run the task. //! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead. diff --git a/launcher/tools/BaseExternalTool.cpp b/launcher/tools/BaseExternalTool.cpp index 9e4b91cd8..dd1a683b4 100644 --- a/launcher/tools/BaseExternalTool.cpp +++ b/launcher/tools/BaseExternalTool.cpp @@ -9,13 +9,13 @@ #include "BaseInstance.h" -BaseExternalTool::BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) +BaseExternalTool::BaseExternalTool(SettingsObject* settings, BaseInstance* instance, QObject* parent) : QObject(parent), m_instance(instance), globalSettings(settings) {} BaseExternalTool::~BaseExternalTool() {} -BaseDetachedTool::BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) +BaseDetachedTool::BaseDetachedTool(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseExternalTool(settings, instance, parent) {} @@ -26,7 +26,7 @@ void BaseDetachedTool::run() BaseExternalToolFactory::~BaseExternalToolFactory() {} -BaseDetachedTool* BaseDetachedToolFactory::createDetachedTool(InstancePtr instance, QObject* parent) +BaseDetachedTool* BaseDetachedToolFactory::createDetachedTool(BaseInstance* instance, QObject* parent) { return qobject_cast(createTool(instance, parent)); } diff --git a/launcher/tools/BaseExternalTool.h b/launcher/tools/BaseExternalTool.h index eb2d07e1e..0890c8e5f 100644 --- a/launcher/tools/BaseExternalTool.h +++ b/launcher/tools/BaseExternalTool.h @@ -10,18 +10,18 @@ class QProcess; class BaseExternalTool : public QObject { Q_OBJECT public: - explicit BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + explicit BaseExternalTool(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); virtual ~BaseExternalTool(); protected: - InstancePtr m_instance; - SettingsObjectPtr globalSettings; + BaseInstance* m_instance; + SettingsObject* globalSettings; }; class BaseDetachedTool : public BaseExternalTool { Q_OBJECT public: - explicit BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + explicit BaseDetachedTool(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); public slots: void run(); @@ -36,18 +36,18 @@ class BaseExternalToolFactory { virtual QString name() const = 0; - virtual void registerSettings(SettingsObjectPtr settings) = 0; + virtual void registerSettings(SettingsObject* settings) = 0; - virtual BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) = 0; + virtual BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) = 0; virtual bool check(QString* error) = 0; virtual bool check(const QString& path, QString* error) = 0; protected: - SettingsObjectPtr globalSettings; + SettingsObject* globalSettings; }; class BaseDetachedToolFactory : public BaseExternalToolFactory { public: - virtual BaseDetachedTool* createDetachedTool(InstancePtr instance, QObject* parent = 0); + virtual BaseDetachedTool* createDetachedTool(BaseInstance* instance, QObject* parent = 0); }; diff --git a/launcher/tools/BaseProfiler.cpp b/launcher/tools/BaseProfiler.cpp index 2ab1254e9..f7a30fa2c 100644 --- a/launcher/tools/BaseProfiler.cpp +++ b/launcher/tools/BaseProfiler.cpp @@ -3,10 +3,10 @@ #include -BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) : BaseExternalTool(settings, instance, parent) +BaseProfiler::BaseProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseExternalTool(settings, instance, parent) {} -void BaseProfiler::beginProfiling(shared_qobject_ptr process) +void BaseProfiler::beginProfiling(LaunchTask* process) { beginProfilingImpl(process); } @@ -27,7 +27,7 @@ void BaseProfiler::abortProfilingImpl() emit abortLaunch(tr("Profiler aborted")); } -BaseProfiler* BaseProfilerFactory::createProfiler(InstancePtr instance, QObject* parent) +BaseProfiler* BaseProfilerFactory::createProfiler(BaseInstance* instance, QObject* parent) { return qobject_cast(createTool(instance, parent)); } diff --git a/launcher/tools/BaseProfiler.h b/launcher/tools/BaseProfiler.h index ac0f3a786..b84a591d7 100644 --- a/launcher/tools/BaseProfiler.h +++ b/launcher/tools/BaseProfiler.h @@ -11,16 +11,16 @@ class QProcess; class BaseProfiler : public BaseExternalTool { Q_OBJECT public: - explicit BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + explicit BaseProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); public slots: - void beginProfiling(shared_qobject_ptr process); + void beginProfiling(LaunchTask* process); void abortProfiling(); protected: QProcess* m_profilerProcess; - virtual void beginProfilingImpl(shared_qobject_ptr process) = 0; + virtual void beginProfilingImpl(LaunchTask* process) = 0; virtual void abortProfilingImpl(); signals: @@ -30,5 +30,5 @@ class BaseProfiler : public BaseExternalTool { class BaseProfilerFactory : public BaseExternalToolFactory { public: - virtual BaseProfiler* createProfiler(InstancePtr instance, QObject* parent = 0); + virtual BaseProfiler* createProfiler(BaseInstance* instance, QObject* parent = 0); }; diff --git a/launcher/tools/GenericProfiler.cpp b/launcher/tools/GenericProfiler.cpp index f7b37b2b8..66c63d01a 100644 --- a/launcher/tools/GenericProfiler.cpp +++ b/launcher/tools/GenericProfiler.cpp @@ -24,22 +24,22 @@ class GenericProfiler : public BaseProfiler { Q_OBJECT public: - GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + GenericProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); protected: - void beginProfilingImpl(shared_qobject_ptr process); + void beginProfilingImpl(LaunchTask* process); }; -GenericProfiler::GenericProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) +GenericProfiler::GenericProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} -void GenericProfiler::beginProfilingImpl(shared_qobject_ptr process) +void GenericProfiler::beginProfilingImpl(LaunchTask* process) { emit readyToLaunch(tr("Started process: %1").arg(process->pid())); } -BaseExternalTool* GenericProfilerFactory::createTool(InstancePtr instance, QObject* parent) +BaseExternalTool* GenericProfilerFactory::createTool(BaseInstance* instance, QObject* parent) { return new GenericProfiler(globalSettings, instance, parent); } diff --git a/launcher/tools/GenericProfiler.h b/launcher/tools/GenericProfiler.h index 7868990ea..49ce7271c 100644 --- a/launcher/tools/GenericProfiler.h +++ b/launcher/tools/GenericProfiler.h @@ -22,8 +22,8 @@ class GenericProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "Generic"; } - void registerSettings([[maybe_unused]] SettingsObjectPtr settings) override {}; - BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; + void registerSettings([[maybe_unused]] SettingsObject* settings) override {}; + BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check([[maybe_unused]] QString* error) override { return true; }; bool check([[maybe_unused]] const QString& path, [[maybe_unused]] QString* error) override { return true; }; }; diff --git a/launcher/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp index 8550038d2..5d51cde97 100644 --- a/launcher/tools/JProfiler.cpp +++ b/launcher/tools/JProfiler.cpp @@ -9,20 +9,20 @@ class JProfiler : public BaseProfiler { Q_OBJECT public: - JProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + JProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); private slots: void profilerStarted(); void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(shared_qobject_ptr process); + void beginProfilingImpl(LaunchTask* process); private: int listeningPort = 0; }; -JProfiler::JProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} +JProfiler::JProfiler(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} void JProfiler::profilerStarted() { @@ -40,7 +40,7 @@ void JProfiler::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus } } -void JProfiler::beginProfilingImpl(shared_qobject_ptr process) +void JProfiler::beginProfilingImpl(LaunchTask* process) { listeningPort = globalSettings->get("JProfilerPort").toInt(); QProcess* profiler = new QProcess(this); @@ -63,14 +63,14 @@ void JProfiler::beginProfilingImpl(shared_qobject_ptr process) profiler->start(); } -void JProfilerFactory::registerSettings(SettingsObjectPtr settings) +void JProfilerFactory::registerSettings(SettingsObject* settings) { settings->registerSetting("JProfilerPath"); settings->registerSetting("JProfilerPort", 42042); globalSettings = settings; } -BaseExternalTool* JProfilerFactory::createTool(InstancePtr instance, QObject* parent) +BaseExternalTool* JProfilerFactory::createTool(BaseInstance* instance, QObject* parent) { return new JProfiler(globalSettings, instance, parent); } diff --git a/launcher/tools/JProfiler.h b/launcher/tools/JProfiler.h index 55715df32..4e6975c25 100644 --- a/launcher/tools/JProfiler.h +++ b/launcher/tools/JProfiler.h @@ -5,8 +5,8 @@ class JProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "JProfiler"; } - void registerSettings(SettingsObjectPtr settings) override; - BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; + void registerSettings(SettingsObject* settings) override; + BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check(QString* error) override; bool check(const QString& path, QString* error) override; }; diff --git a/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp index 2e1cf69f7..9155a1832 100644 --- a/launcher/tools/JVisualVM.cpp +++ b/launcher/tools/JVisualVM.cpp @@ -10,17 +10,17 @@ class JVisualVM : public BaseProfiler { Q_OBJECT public: - JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject* parent = 0); + JVisualVM(SettingsObject* settings, BaseInstance* instance, QObject* parent = 0); private slots: void profilerStarted(); void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(shared_qobject_ptr process); + void beginProfilingImpl(LaunchTask* process); }; -JVisualVM::JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} +JVisualVM::JVisualVM(SettingsObject* settings, BaseInstance* instance, QObject* parent) : BaseProfiler(settings, instance, parent) {} void JVisualVM::profilerStarted() { @@ -38,7 +38,7 @@ void JVisualVM::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus } } -void JVisualVM::beginProfilingImpl(shared_qobject_ptr process) +void JVisualVM::beginProfilingImpl(LaunchTask* process) { QProcess* profiler = new QProcess(this); QStringList profilerArgs = { "--openpid", QString::number(process->pid()) }; @@ -54,7 +54,7 @@ void JVisualVM::beginProfilingImpl(shared_qobject_ptr process) m_profilerProcess = profiler; } -void JVisualVMFactory::registerSettings(SettingsObjectPtr settings) +void JVisualVMFactory::registerSettings(SettingsObject* settings) { QString defaultValue = QStandardPaths::findExecutable("jvisualvm"); if (defaultValue.isNull()) { @@ -64,7 +64,7 @@ void JVisualVMFactory::registerSettings(SettingsObjectPtr settings) globalSettings = settings; } -BaseExternalTool* JVisualVMFactory::createTool(InstancePtr instance, QObject* parent) +BaseExternalTool* JVisualVMFactory::createTool(BaseInstance* instance, QObject* parent) { return new JVisualVM(globalSettings, instance, parent); } diff --git a/launcher/tools/JVisualVM.h b/launcher/tools/JVisualVM.h index c152aecdb..dfb09caf4 100644 --- a/launcher/tools/JVisualVM.h +++ b/launcher/tools/JVisualVM.h @@ -5,8 +5,8 @@ class JVisualVMFactory : public BaseProfilerFactory { public: QString name() const override { return "VisualVM"; } - void registerSettings(SettingsObjectPtr settings) override; - BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; + void registerSettings(SettingsObject* settings) override; + BaseExternalTool* createTool(BaseInstance* instance, QObject* parent = 0) override; bool check(QString* error) override; bool check(const QString& path, QString* error) override; }; diff --git a/launcher/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp index e006a1411..12db12e42 100644 --- a/launcher/tools/MCEditTool.cpp +++ b/launcher/tools/MCEditTool.cpp @@ -8,7 +8,7 @@ #include "minecraft/MinecraftInstance.h" #include "settings/SettingsObject.h" -MCEditTool::MCEditTool(SettingsObjectPtr settings) +MCEditTool::MCEditTool(SettingsObject* settings) { settings->registerSetting("MCEditPath"); m_settings = settings; diff --git a/launcher/tools/MCEditTool.h b/launcher/tools/MCEditTool.h index fd2de1b6d..edc9ffa27 100644 --- a/launcher/tools/MCEditTool.h +++ b/launcher/tools/MCEditTool.h @@ -5,12 +5,12 @@ class MCEditTool { public: - MCEditTool(SettingsObjectPtr settings); + MCEditTool(SettingsObject* settings); void setPath(QString& path); QString path() const; bool check(const QString& toolPath, QString& error); QString getProgramPath(); private: - SettingsObjectPtr m_settings; + SettingsObject* m_settings; }; diff --git a/launcher/translations/POTranslator.cpp b/launcher/translations/POTranslator.cpp index e4e2573c6..0bd3638e6 100644 --- a/launcher/translations/POTranslator.cpp +++ b/launcher/translations/POTranslator.cpp @@ -133,7 +133,7 @@ void POTranslatorPrivate::reload() { QFile file(filename); if (!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text)) { - qDebug() << "Failed to open PO file:" << filename; + qDebug() << "Failed to open PO file:" << filename << "error:" << file.errorString(); return; } @@ -183,8 +183,7 @@ void POTranslatorPrivate::reload() nextFuzzy = true; } } else if (line.startsWith('"')) { - QByteArray temp; - QByteArray* out = &temp; + QByteArray* out = nullptr; switch (mode) { case Mode::First: diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index ee2e240ab..7f905608b 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -36,13 +36,9 @@ #include "TranslationsModel.h" -#include #include -#include -#include -#include -#include -#include +#include +#include #include "BuildConfig.h" #include "FileSystem.h" @@ -53,19 +49,28 @@ #include "POTranslator.h" #include "Application.h" +#include "settings/SettingsObject.h" -const static QLatin1String defaultLangCode("en_US"); +static constexpr QLatin1String g_defaultLangCode("en_US"); -enum class FileType { NONE, QM, PO }; +namespace { +enum class FileType : std::uint8_t { None, Qm, Po }; + +QString getSystemLocaleName() +{ + return QLocale::system().name(); +} + +QString getSystemLanguage() +{ + return getSystemLocaleName().split('_').front(); +} +} // namespace struct Language { - Language() { updated = true; } - Language(const QString& _key) - { - key = _key; - locale = QLocale(key); - updated = (key == defaultLangCode); - } + Language() : updated(true) {} + + explicit Language(QString _key) : key(std::move(_key)), updated(key == g_defaultLangCode) { locale = QLocale(key); } QString languageName() const { @@ -97,12 +102,12 @@ struct Language { float percentTranslated() const { if (total == 0) { - return 100.0f; + return 100.0F; } - return 100.0f * float(translated) / float(total); + return 100.0F * static_cast(translated) / static_cast(total); } - void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy) + void setTranslationStats(const unsigned _translated, const unsigned _untranslated, const unsigned _fuzzy) { translated = _translated; untranslated = _untranslated; @@ -114,18 +119,18 @@ struct Language { bool isIdenticalTo(const Language& other) const { - return (key == other.key && file_name == other.file_name && file_size == other.file_size && file_sha1 == other.file_sha1 && + return (key == other.key && fileName == other.fileName && fileSize == other.fileSize && fileSha1 == other.fileSha1 && translated == other.translated && fuzzy == other.fuzzy && total == other.fuzzy && localFileType == other.localFileType); } - Language& apply(Language& other) + Language& apply(const Language& other) { if (!isOfSameNameAs(other)) { return *this; } - file_name = other.file_name; - file_size = other.file_size; - file_sha1 = other.file_sha1; + fileName = other.fileName; + fileSize = other.fileSize; + fileSha1 = other.fileSha1; translated = other.translated; fuzzy = other.fuzzy; total = other.total; @@ -137,47 +142,44 @@ struct Language { QLocale locale; bool updated; - QString file_name = QString(); - std::size_t file_size = 0; - QString file_sha1 = QString(); + QString fileName = QString(); + std::size_t fileSize = 0; + QString fileSha1 = QString(); unsigned translated = 0; unsigned untranslated = 0; unsigned fuzzy = 0; unsigned total = 0; - FileType localFileType = FileType::NONE; + FileType localFileType = FileType::None; }; struct TranslationsModel::Private { QDir m_dir; // initial state is just english - QList m_languages = { Language(defaultLangCode) }; + QList m_languages = { Language(g_defaultLangCode) }; - QString m_selectedLanguage = defaultLangCode; - std::unique_ptr m_qt_translator; - std::unique_ptr m_app_translator; + QString m_selectedLanguage = g_defaultLangCode; + std::unique_ptr m_qtTranslator; + std::unique_ptr m_appTranslator; - Net::Download* m_index_task; + Net::Download* m_indexTask = nullptr; QString m_downloadingTranslation; - NetJob::Ptr m_dl_job; - NetJob::Ptr m_index_job; + NetJob::Ptr m_downloadJob; + NetJob::Ptr m_indexJob; QString m_nextDownload; - std::unique_ptr m_po_translator; - QFileSystemWatcher* watcher; + QFileSystemWatcher* watcher = nullptr; - const QString m_system_locale = QLocale::system().name(); - const QString m_system_language = m_system_locale.split('_').front(); - - bool no_language_set = false; + bool m_noLanguageSet = false; }; -TranslationsModel::TranslationsModel(QString path, QObject* parent) : QAbstractListModel(parent) +TranslationsModel::TranslationsModel(const QString& path, QObject* parent) : QAbstractListModel(parent) { - d.reset(new Private); + d = std::make_unique(); d->m_dir.setPath(path); + d->m_selectedLanguage = APPLICATION->settings()->get("Language").toString(); FS::ensureFolderPathExists(path); reloadLocalFiles(); @@ -186,12 +188,12 @@ TranslationsModel::TranslationsModel(QString path, QObject* parent) : QAbstractL d->watcher->addPath(d->m_dir.canonicalPath()); } -TranslationsModel::~TranslationsModel() {} +TranslationsModel::~TranslationsModel() = default; void TranslationsModel::translationDirChanged(const QString& path) { qDebug() << "Dir changed:" << path; - if (!d->no_language_set) { + if (!d->m_noLanguageSet) { reloadLocalFiles(); } selectLanguage(selectedLanguage()); @@ -200,25 +202,21 @@ void TranslationsModel::translationDirChanged(const QString& path) void TranslationsModel::indexReceived() { qDebug() << "Got translations index!"; - d->m_index_job.reset(); + d->m_indexJob.reset(); + reloadLocalFiles(); - if (d->no_language_set) { - reloadLocalFiles(); - - auto language = d->m_system_locale; + if (d->m_noLanguageSet) { + auto language = getSystemLocaleName(); if (!findLanguageAsOptional(language).has_value()) { - language = d->m_system_language; + language = getSystemLanguage(); } selectLanguage(language); - if (selectedLanguage() != defaultLangCode) { - updateLanguage(selectedLanguage()); - } APPLICATION->settings()->set("Language", selectedLanguage()); - d->no_language_set = false; + d->m_noLanguageSet = false; } - else if (d->m_selectedLanguage != defaultLangCode) { - downloadTranslation(d->m_selectedLanguage); + if (selectedLanguage() != g_defaultLangCode) { + updateLanguage(selectedLanguage()); } } @@ -234,27 +232,27 @@ void readIndex(const QString& path, QMap& languages) } try { - auto toplevel_doc = Json::requireDocument(data); - auto doc = Json::requireObject(toplevel_doc); - auto file_type = Json::requireString(doc, "file_type"); - if (file_type != "MMC-TRANSLATION-INDEX") { - qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type; + auto toplevelDoc = Json::requireDocument(data); + auto doc = Json::requireObject(toplevelDoc); + auto fileType = Json::requireString(doc, "file_type"); + if (fileType != "MMC-TRANSLATION-INDEX") { + qCritical() << "Translations Download Failed: index file is of unknown file type" << fileType; return; } auto version = Json::requireInteger(doc, "version"); if (version > 2) { - qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type; + qCritical() << "Translations Download Failed: index file is of unknown format version" << fileType; return; } auto langObjs = Json::requireObject(doc, "languages"); - for (auto iter = langObjs.begin(); iter != langObjs.end(); iter++) { + for (auto iter = langObjs.begin(); iter != langObjs.end(); ++iter) { Language lang(iter.key()); auto langObj = Json::requireObject(iter.value()); lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(), langObj["fuzzy"].toInt()); - lang.file_name = Json::requireString(langObj, "file"); - lang.file_sha1 = Json::requireString(langObj, "sha1"); - lang.file_size = Json::requireInteger(langObj, "size"); + lang.fileName = Json::requireString(langObj, "file"); + lang.fileSha1 = Json::requireString(langObj, "sha1"); + lang.fileSize = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); } @@ -266,20 +264,25 @@ void readIndex(const QString& path, QMap& languages) void TranslationsModel::reloadLocalFiles() { - QMap languages = { { defaultLangCode, Language(defaultLangCode) } }; + QMap languages = { { g_defaultLangCode, Language(g_defaultLangCode) } }; - readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages); + const auto indexPath = d->m_dir.absoluteFilePath("index_v2.json"); + if (!QFileInfo::exists(indexPath)) { + downloadIndex(); + return; + } + readIndex(indexPath, languages); auto entries = d->m_dir.entryInfoList({ "mmc_*.qm", "*.po" }, QDir::Files | QDir::NoDotAndDotDot); for (auto& entry : entries) { auto completeSuffix = entry.completeSuffix(); QString langCode; - FileType fileType = FileType::NONE; + FileType fileType = FileType::None; if (completeSuffix == "qm") { langCode = entry.baseName().remove(0, 4); - fileType = FileType::QM; + fileType = FileType::Qm; } else if (completeSuffix == "po") { langCode = entry.baseName(); - fileType = FileType::PO; + fileType = FileType::Po; } else { continue; } @@ -287,13 +290,14 @@ void TranslationsModel::reloadLocalFiles() auto langIter = languages.find(langCode); if (langIter != languages.end()) { auto& language = *langIter; - if (int(fileType) > int(language.localFileType)) { + // TODO: use std::to_underlying in C++23 + if (static_cast(fileType) > static_cast(language.localFileType)) { language.localFileType = fileType; } } else { - if (fileType == FileType::PO) { + if (fileType == FileType::Po) { Language localFound(langCode); - localFound.localFileType = FileType::PO; + localFound.localFileType = FileType::Po; languages.insert(langCode, localFound); } } @@ -313,7 +317,7 @@ void TranslationsModel::reloadLocalFiles() emit dataChanged(index(row), index(row)); languages.remove(language.key); } - iter++; + ++iter; } else { beginRemoveRows(QModelIndex(), row, row); iter = d->m_languages.erase(iter); @@ -328,34 +332,38 @@ void TranslationsModel::reloadLocalFiles() for (auto& language : languages) { d->m_languages.append(language); } - std::sort(d->m_languages.begin(), d->m_languages.end(), [this](const Language& a, const Language& b) { + + const auto comp = [systemLocale = getSystemLocaleName(), systemLanguage = getSystemLanguage()](const Language& a, const Language& b) { if (a.key != b.key) { - if (a.key == d->m_system_locale || a.key == d->m_system_language) { + if (a.key == systemLocale || a.key == systemLanguage) { return true; } - if (b.key == d->m_system_locale || b.key == d->m_system_language) { + if (b.key == systemLocale || b.key == systemLanguage) { return false; } } return a.languageName().toLower() < b.languageName().toLower(); - }); + }; + std::ranges::sort(d->m_languages, comp); endInsertRows(); } namespace { -enum class Column { Language, Completeness }; +enum class Column : std::uint8_t { Language, Completeness }; } -QVariant TranslationsModel::data(const QModelIndex& index, int role) const +QVariant TranslationsModel::data(const QModelIndex& index, const int role) const { - if (!index.isValid()) - return QVariant(); + if (!index.isValid()) { + return {}; + } - int row = index.row(); - auto column = static_cast(index.column()); + const int row = index.row(); + const auto column = static_cast(index.column()); - if (row < 0 || row >= d->m_languages.size()) - return QVariant(); + if (row < 0 || row >= d->m_languages.size()) { + return {}; + } auto& lang = d->m_languages[row]; switch (role) { @@ -377,11 +385,11 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const case Qt::UserRole: return lang.key; default: - return QVariant(); + return {}; } } -QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant TranslationsModel::headerData(int section, const Qt::Orientation orientation, const int role) const { auto column = static_cast(section); if (role == Qt::DisplayRole) { @@ -416,49 +424,50 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c return 2; } -QList::Iterator TranslationsModel::findLanguage(const QString& key) +QList::Iterator TranslationsModel::findLanguage(const QString& key) const { - return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; }); + return std::ranges::find_if(d->m_languages, [key](const Language& lang) { return lang.key == key; }); } -std::optional TranslationsModel::findLanguageAsOptional(const QString& key) +std::optional TranslationsModel::findLanguageAsOptional(const QString& key) const { auto found = findLanguage(key); - if (found != d->m_languages.end()) + if (found != d->m_languages.end()) { return *found; + } return {}; } -void TranslationsModel::setUseSystemLocale(bool useSystemLocale) +void TranslationsModel::setUseSystemLocale(const bool useSystemLocale) const { APPLICATION->settings()->set("UseSystemLocale", useSystemLocale); - QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode)); + QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(selectedLanguage())); } -bool TranslationsModel::selectLanguage(QString key) +bool TranslationsModel::selectLanguage(QString key) const { QString& langCode = key; auto langPtr = findLanguageAsOptional(key); if (langCode.isEmpty()) { - d->no_language_set = true; + d->m_noLanguageSet = true; } if (!langPtr.has_value()) { - qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; - langCode = defaultLangCode; + qWarning() << "Selected invalid language" << key << ", defaulting to" << g_defaultLangCode; + langCode = g_defaultLangCode; } else { langCode = langPtr->key; } // uninstall existing translators if there are any - if (d->m_app_translator) { - QCoreApplication::removeTranslator(d->m_app_translator.get()); - d->m_app_translator.reset(); + if (d->m_appTranslator) { + QCoreApplication::removeTranslator(d->m_appTranslator.get()); + d->m_appTranslator.reset(); } - if (d->m_qt_translator) { - QCoreApplication::removeTranslator(d->m_qt_translator.get()); - d->m_qt_translator.reset(); + if (d->m_qtTranslator) { + QCoreApplication::removeTranslator(d->m_qtTranslator.get()); + d->m_qtTranslator.reset(); } /* @@ -466,11 +475,11 @@ bool TranslationsModel::selectLanguage(QString key) * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ - QLocale::setDefault( - QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode)); + const bool useSystemLocale = APPLICATION->settings()->get("UseSystemLocale").toBool(); + QLocale::setDefault(useSystemLocale ? QLocale::system() : QLocale(langCode)); // if it's the default UI language, finish - if (langCode == defaultLangCode) { + if (langCode == g_defaultLangCode) { d->m_selectedLanguage = langCode; return true; } @@ -478,89 +487,88 @@ bool TranslationsModel::selectLanguage(QString key) // otherwise install new translations bool successful = false; // FIXME: this is likely never present. FIX IT. - d->m_qt_translator.reset(new QTranslator()); - if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { + d->m_qtTranslator = std::make_unique(); + if (d->m_qtTranslator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; - if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) { + if (!QCoreApplication::installTranslator(d->m_qtTranslator.get())) { qCritical() << "Loading Qt Language File failed."; - d->m_qt_translator.reset(); + d->m_qtTranslator.reset(); } else { successful = true; } } else { - d->m_qt_translator.reset(); + d->m_qtTranslator.reset(); } - if (langPtr->localFileType == FileType::PO) { + if (langPtr->localFileType == FileType::Po) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; - auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); - if (!poTranslator->isEmpty()) { - if (!QCoreApplication::installTranslator(poTranslator)) { - delete poTranslator; + d->m_appTranslator = std::make_unique(FS::PathCombine(d->m_dir.path(), langCode + ".po")); + if (!d->m_appTranslator->isEmpty()) { + if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) { qCritical() << "Installing Application Language File failed."; + d->m_appTranslator.reset(); } else { - d->m_app_translator.reset(poTranslator); successful = true; } } else { qCritical() << "Loading Application Language File failed."; - d->m_app_translator.reset(); + d->m_appTranslator.reset(); } - } else if (langPtr->localFileType == FileType::QM) { - d->m_app_translator.reset(new QTranslator()); - if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) { + } else if (langPtr->localFileType == FileType::Qm) { + d->m_appTranslator = std::make_unique(); + if (d->m_appTranslator->load("mmc_" + langCode, d->m_dir.path())) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; - if (!QCoreApplication::installTranslator(d->m_app_translator.get())) { + if (!QCoreApplication::installTranslator(d->m_appTranslator.get())) { qCritical() << "Installing Application Language File failed."; - d->m_app_translator.reset(); + d->m_appTranslator.reset(); } else { successful = true; } } else { - d->m_app_translator.reset(); + d->m_appTranslator.reset(); } } else { - d->m_app_translator.reset(); + d->m_appTranslator.reset(); } d->m_selectedLanguage = langCode; return successful; } -QModelIndex TranslationsModel::selectedIndex() +QModelIndex TranslationsModel::selectedIndex() const { auto found = findLanguage(d->m_selectedLanguage); if (found != d->m_languages.end()) { return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex()); } - return QModelIndex(); + return {}; } -QString TranslationsModel::selectedLanguage() +QString TranslationsModel::selectedLanguage() const { return d->m_selectedLanguage; } void TranslationsModel::downloadIndex() { - if (d->m_index_job || d->m_dl_job) { + if (d->m_indexJob || d->m_downloadJob) { return; } qDebug() << "Downloading Translations Index..."; - d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); + d->m_indexJob.reset(new NetJob("Translations Index", APPLICATION->network())); + const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); - d->m_index_task = task.get(); - d->m_index_job->addNetAction(task); - d->m_index_job->setAskRetry(false); - connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); - connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); - d->m_index_job->start(); + d->m_indexTask = task.get(); + d->m_indexJob->addNetAction(task); + d->m_indexJob->setAskRetry(false); + connect(d->m_indexJob.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); + connect(d->m_indexJob.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); + d->m_indexJob->start(); } -void TranslationsModel::updateLanguage(QString key) +void TranslationsModel::updateLanguage(const QString& key) { - if (key == defaultLangCode) { + if (key == g_defaultLangCode) { qWarning() << "Cannot update builtin language" << key; return; } @@ -574,9 +582,9 @@ void TranslationsModel::updateLanguage(QString key) } } -void TranslationsModel::downloadTranslation(QString key) +void TranslationsModel::downloadTranslation(const QString& key) { - if (d->m_dl_job) { + if (d->m_downloadJob) { d->m_nextDownload = key; return; } @@ -587,21 +595,21 @@ void TranslationsModel::downloadTranslation(QString key) } d->m_downloadingTranslation = key; - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); + const MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); - auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); - dl->setProgress(dl->getProgress(), lang->file_size); + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->fileName), entry); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->fileSha1)); + dl->setProgress(dl->getProgress(), lang->fileSize); - d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); - d->m_dl_job->addNetAction(dl); - d->m_dl_job->setAskRetry(false); + d->m_downloadJob.reset(new NetJob("Translation for " + key, APPLICATION->network())); + d->m_downloadJob->addNetAction(dl); + d->m_downloadJob->setAskRetry(false); - connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); - connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); + connect(d->m_downloadJob.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); + connect(d->m_downloadJob.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); - d->m_dl_job->start(); + d->m_downloadJob->start(); } void TranslationsModel::downloadNext() @@ -612,10 +620,10 @@ void TranslationsModel::downloadNext() } } -void TranslationsModel::dlFailed(QString reason) +void TranslationsModel::dlFailed(const QString& reason) { qCritical() << "Translations Download Failed:" << reason; - d->m_dl_job.reset(); + d->m_downloadJob.reset(); downloadNext(); } @@ -626,12 +634,12 @@ void TranslationsModel::dlGood() if (d->m_downloadingTranslation == d->m_selectedLanguage) { selectLanguage(d->m_selectedLanguage); } - d->m_dl_job.reset(); + d->m_downloadJob.reset(); downloadNext(); } -void TranslationsModel::indexFailed(QString reason) +void TranslationsModel::indexFailed(const QString& reason) const { qCritical() << "Translations Index Download Failed:" << reason; - d->m_index_job.reset(); + d->m_indexJob.reset(); } diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 945e689fc..1dd0b601c 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -24,38 +24,38 @@ struct Language; class TranslationsModel : public QAbstractListModel { Q_OBJECT public: - explicit TranslationsModel(QString path, QObject* parent = 0); - virtual ~TranslationsModel(); - - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent) const override; - - bool selectLanguage(QString key); - void updateLanguage(QString key); - QModelIndex selectedIndex(); - QString selectedLanguage(); - - void downloadIndex(); - void setUseSystemLocale(bool useSystemLocale); - - private: - QList::Iterator findLanguage(const QString& key); - std::optional findLanguageAsOptional(const QString& key); - void reloadLocalFiles(); - void downloadTranslation(QString key); - void downloadNext(); + explicit TranslationsModel(const QString& path, QObject* parent = nullptr); + ~TranslationsModel() override; // hide copy constructor TranslationsModel(const TranslationsModel&) = delete; // hide assign op TranslationsModel& operator=(const TranslationsModel&) = delete; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; // NOLINT(*-default-arguments) + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; // NOLINT(*-default-arguments) + bool selectLanguage(QString key) const; + void updateLanguage(const QString& key); + QModelIndex selectedIndex() const; + QString selectedLanguage() const; + + void downloadIndex(); + void setUseSystemLocale(bool useSystemLocale) const; + + private: + int columnCount(const QModelIndex& parent) const override; + + QList::Iterator findLanguage(const QString& key) const; + std::optional findLanguageAsOptional(const QString& key) const; + void reloadLocalFiles(); + void downloadTranslation(const QString& key); + void downloadNext(); + private slots: void indexReceived(); - void indexFailed(QString reason); - void dlFailed(QString reason); + void indexFailed(const QString& reason) const; + void dlFailed(const QString& reason); void dlGood(); void translationDirChanged(const QString& path); diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 93da1d10c..a164351b0 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -49,7 +49,7 @@ #include "icons/IconList.h" -InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWindow(parent), m_instance(instance) +InstanceWindow::InstanceWindow(BaseInstance* instance, QWidget* parent) : QMainWindow(parent), m_instance(instance) { setAttribute(Qt::WA_DeleteOnClose); @@ -109,7 +109,7 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin m_container->addButtons(horizontalLayout); - connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); + connect(m_instance, &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceWindow::updateButtons); } @@ -125,13 +125,13 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin { auto launchTask = m_instance->getLaunchTask(); instanceLaunchTaskChanged(launchTask); - connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::instanceLaunchTaskChanged); - connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::runningStateChanged); + connect(m_instance, &BaseInstance::launchTaskChanged, this, &InstanceWindow::instanceLaunchTaskChanged); + connect(m_instance, &BaseInstance::runningStatusChanged, this, &InstanceWindow::runningStateChanged); } // set up instance destruction detection { - connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); + connect(m_instance, &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); } // add ourself as the modpack page's instance window @@ -164,7 +164,7 @@ void InstanceWindow::updateButtons() m_launchButton->setMenu(launchMenu); } -void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr proc) +void InstanceWindow::instanceLaunchTaskChanged(LaunchTask* proc) { m_proc = proc; } diff --git a/launcher/ui/InstanceWindow.h b/launcher/ui/InstanceWindow.h index e5bc24d44..7f66a8b9c 100644 --- a/launcher/ui/InstanceWindow.h +++ b/launcher/ui/InstanceWindow.h @@ -53,7 +53,7 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { Q_OBJECT public: - explicit InstanceWindow(InstancePtr proc, QWidget* parent = 0); + explicit InstanceWindow(BaseInstance* proc, QWidget* parent = 0); virtual ~InstanceWindow() = default; bool selectPage(QString pageId) override; @@ -72,7 +72,7 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void isClosing(); private slots: - void instanceLaunchTaskChanged(shared_qobject_ptr proc); + void instanceLaunchTaskChanged(LaunchTask* proc); void runningStateChanged(bool running); void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); @@ -83,8 +83,8 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void updateButtons(); private: - shared_qobject_ptr m_proc; - InstancePtr m_instance; + LaunchTask* m_proc; + BaseInstance* m_instance; bool m_doNotSave = false; PageContainer* m_container = nullptr; QPushButton* m_closeButton = nullptr; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 77a480e70..1bdcd3f68 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include @@ -104,6 +105,7 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/skins/SkinManageDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" @@ -179,7 +181,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance); // restore the instance toolbar settings - auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); + const auto setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toString().toUtf8())); @@ -322,14 +324,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection); proxymodel = new InstanceProxyModel(this); - proxymodel->setSourceModel(APPLICATION->instances().get()); + proxymodel->setSourceModel(APPLICATION->instances()); proxymodel->sort(0); connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged); view->setModel(proxymodel); view->setSourceOfGroupCollapseStatus( [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); }); - connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged); + connect(view, &InstanceView::groupStateChanged, APPLICATION->instances(), &InstanceList::on_GroupStateChanged); ui->horizontalLayout->addWidget(view); } // The cat background @@ -365,13 +367,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::instanceChanged); // track icon changes and update the toolbar! - connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, &MainWindow::iconUpdated); + connect(APPLICATION->icons(), &IconList::iconUpdated, this, &MainWindow::iconUpdated); // model reset -> selection is invalid. All the instance pointers are wrong. - connect(APPLICATION->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad); + connect(APPLICATION->instances(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad); // handle newly added instances - connect(APPLICATION->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest); + connect(APPLICATION->instances(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest); // When the global settings page closes, we want to know about it and update our state connect(APPLICATION, &Application::globalSettingsApplied, this, &MainWindow::globalSettingsClosed); @@ -394,8 +396,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... - connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); - connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { defaultAccountChanged(); }); + connect(APPLICATION->accounts(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); + connect(APPLICATION->accounts(), &AccountList::listActivityChanged, [this] { defaultAccountChanged(); }); + connect(APPLICATION->accounts(), &AccountList::listChanged, [this] { defaultAccountChanged(); }); // Show initial account defaultAccountChanged(); @@ -419,7 +422,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi auto updater = APPLICATION->updater(); if (updater) { - connect(updater.get(), &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged); + connect(updater, &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged); } } @@ -457,7 +460,7 @@ void MainWindow::retranslateUi() MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); if (defaultAccount) { - auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); + auto profileLabel = profileInUseFilter(defaultAccount->displayName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } @@ -652,12 +655,15 @@ void MainWindow::repopulateAccountsMenu() auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + + bool canChangeSkin = defaultAccount && (defaultAccount->accountType() == AccountType::MSA) && !defaultAccount->isActive(); + ui->actionManageSkins->setEnabled(canChangeSkin); QString active_profileId = ""; if (defaultAccount) { // this can be called before accountMenuButton exists if (ui->actionAccountsButton) { - auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); + auto profileLabel = profileInUseFilter(defaultAccount->displayName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } } @@ -671,7 +677,7 @@ void MainWindow::repopulateAccountsMenu() // TODO: Nicer way to iterate? for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); - auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + auto profileLabel = profileInUseFilter(account->displayName(), account->isInUse()); QAction* action = new QAction(profileLabel, this); action->setData(i); action->setCheckable(true); @@ -708,6 +714,7 @@ void MainWindow::repopulateAccountsMenu() connect(ui->actionNoDefaultAccount, &QAction::triggered, this, &MainWindow::changeActiveAccount); ui->accountsMenu->addSeparator(); + ui->accountsMenu->addAction(ui->actionManageSkins); ui->accountsMenu->addAction(ui->actionManageAccounts); accountsButtonMenu->addActions(ui->accountsMenu->actions()); @@ -751,7 +758,7 @@ void MainWindow::defaultAccountChanged() // FIXME: this needs adjustment for MSA if (account && account->profileName() != "") { - auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + auto profileLabel = profileInUseFilter(account->displayName(), account->isInUse()); ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if (face.isNull()) { @@ -930,6 +937,9 @@ void MainWindow::processURLs(QList urls) { // NOTE: This loop only processes one dropped file! for (auto& url : urls) { + if (url.isEmpty() || url.toString().trimmed().isEmpty()) + continue; + qDebug() << "Processing" << url; // The isLocalFile() check below doesn't work as intended without an explicit scheme. @@ -940,12 +950,25 @@ void MainWindow::processURLs(QList urls) QMap extra_info; QUrl local_url; if (!url.isLocalFile()) { // download the remote resource and identify + + const bool isExternalURLImport = (url.host().toLower() == "import") || (url.path().startsWith("/import", Qt::CaseInsensitive)); + QUrl dl_url; - if (url.scheme() == "curseforge") { + if (url.scheme() == "curseforge" || (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME && url.host() == "install")) { // need to find the download link for the modpack / resource // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE + // format of url binaryname://install?platform=curseforge&addonId=IDHERE&fileId=IDHERE QUrlQuery query(url); + // check if this is a binaryname:// url + if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) { + // check this is an curseforge platform request + if (query.queryItemValue("platform").toLower() != "curseforge") { + qDebug() << "Invalid mod distribution platform:" << query.queryItemValue("platform"); + continue; + } + } + if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { qDebug() << "Invalid curseforge link:" << url; continue; @@ -957,10 +980,8 @@ void MainWindow::processURLs(QList urls) extra_info.insert("pack_id", addonId); extra_info.insert("pack_version_id", fileId); - auto array = std::make_shared(); - auto api = FlameAPI(); - auto job = api.getFile(addonId, fileId, array); + auto [job, array] = api.getFile(addonId, fileId); connect(job.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); @@ -993,7 +1014,7 @@ void MainWindow::processURLs(QList urls) dlUrlDialod.execWithTask(job.get()); } - } else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) { + } else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME && !isExternalURLImport) { QVariantMap receivedData; const QUrlQuery query(url.query()); const auto items = query.queryItems(); @@ -1001,6 +1022,65 @@ void MainWindow::processURLs(QList urls) receivedData.insert(it->first, it->second); emit APPLICATION->oauthReplyRecieved(receivedData); continue; + } else if ((url.scheme() == "prismlauncher" || url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) && isExternalURLImport) { + // PrismLauncher URL protocol modpack import + // works for any prism fork + // preferred import format: prismlauncher://import?url=ENCODED + const auto host = url.host().toLower(); + const auto path = url.path(); + + QString encodedTarget; + + { + QUrlQuery query(url); + const auto values = query.allQueryItemValues("url"); + if (!values.isEmpty()) { + encodedTarget = values.first(); + } + } + + // alternative import format: prismlauncher://import/ENCODED + if (encodedTarget.isEmpty()) { + QString p = path; + + if (p.startsWith("/import/", Qt::CaseInsensitive)) { + p = p.mid(QString("/import/").size()); + } else if (host == "import" && p.startsWith("/")) { + p = p.mid(1); + } + + if (!p.isEmpty() && p != "/import") { + encodedTarget = p; + } + } + + if (encodedTarget.isEmpty()) { + CustomMessageBox::selectable(this, tr("Error"), tr("Invalid import link: missing 'url' parameter."), + QMessageBox::Critical) + ->show(); + continue; + } + + const QString decodedStr = QUrl::fromPercentEncoding(encodedTarget.toUtf8()).trimmed(); + + QUrl target = QUrl::fromUserInput(decodedStr); + + // Validate: only allow http(s) + if (!target.isValid() || (target.scheme() != "https" && target.scheme() != "http")) { + CustomMessageBox::selectable(this, tr("Error"), tr("Invalid import link: URL must be http(s)."), QMessageBox::Critical) + ->show(); + continue; + } + + const auto res = QMessageBox::question( + this, tr("Install modpack"), + tr("Do you want to download and import a modpack from:\n%1\n\nURL:\n%2").arg(target.host(), target.toString()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (res != QMessageBox::Yes) { + continue; + } + + dl_url = target; } else { dl_url = url; } @@ -1039,6 +1119,11 @@ void MainWindow::processURLs(QList urls) auto localFileName = QDir::toNativeSeparators(local_url.toLocalFile()); QFileInfo localFileInfo(localFileName); + if (localFileName.isEmpty() || !localFileInfo.exists()) { + qDebug() << "Ignoring invalid path" << localFileName; + continue; + } + auto type = ResourceUtils::identify(localFileInfo); if (ModPlatform::ResourceTypeUtils::VALID_RESOURCES.count(type) == 0) { // probably instance/modpack @@ -1062,7 +1147,7 @@ void MainWindow::processURLs(QList urls) qDebug() << "Adding resource" << localFileName << "to" << dlg.selectedInstanceKey; auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey); - auto minecraftInst = std::dynamic_pointer_cast(inst); + auto minecraftInst = dynamic_cast(inst); switch (type) { case ModPlatform::ResourceType::ResourcePack: @@ -1310,6 +1395,16 @@ void MainWindow::on_actionEditInstance_triggered() } } +void MainWindow::on_actionManageSkins_triggered() +{ + auto account = APPLICATION->accounts()->defaultAccount(); + + if (account && (account->accountType() == AccountType::MSA) && !account->isActive()) { + SkinManageDialog dialog(this, account); + dialog.exec(); + } +} + void MainWindow::on_actionManageAccounts_triggered() { APPLICATION->ShowGlobalSettings(this, "accounts"); @@ -1359,7 +1454,7 @@ void MainWindow::on_actionAddToPATH_triggered() void MainWindow::on_actionOpenWiki_triggered() { - DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(""))); + DesktopServices::openUrl(QUrl(BuildConfig.WIKI_URL)); } void MainWindow::on_actionMoreNews_triggered() @@ -1442,7 +1537,7 @@ void MainWindow::on_actionExportInstanceZip_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered() { if (m_selectedInstance) { - auto instance = std::dynamic_pointer_cast(m_selectedInstance); + auto instance = dynamic_cast(m_selectedInstance); if (instance != nullptr) { ExportPackDialog dlg(instance, this); dlg.exec(); @@ -1453,7 +1548,7 @@ void MainWindow::on_actionExportInstanceMrPack_triggered() void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { - auto instance = std::dynamic_pointer_cast(m_selectedInstance); + auto instance = dynamic_cast(m_selectedInstance); if (instance) { if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { @@ -1506,7 +1601,7 @@ void MainWindow::instanceActivated(QModelIndex index) if (!index.isValid()) return; QString id = index.data(InstanceList::InstanceIDRole).toString(); - InstancePtr inst = APPLICATION->instances()->getInstanceById(id); + BaseInstance* inst = APPLICATION->instances()->getInstanceById(id); if (!inst) return; @@ -1520,7 +1615,7 @@ void MainWindow::on_actionLaunchInstance_triggered() } } -void MainWindow::activateInstance(InstancePtr instance) +void MainWindow::activateInstance(BaseInstance* instance) { APPLICATION->launch(instance); } @@ -1567,8 +1662,8 @@ void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] co return; } if (m_selectedInstance) { - disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); - disconnect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); + disconnect(m_selectedInstance, &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + disconnect(m_selectedInstance, &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); @@ -1588,8 +1683,8 @@ void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] co APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); - connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); - connect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); + connect(m_selectedInstance, &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + connect(m_selectedInstance, &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } else { APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0e692eda7..80860deef 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -130,6 +130,8 @@ class MainWindow : public QMainWindow { void on_actionSettings_triggered(); + void on_actionManageSkins_triggered(); + void on_actionManageAccounts_triggered(); void on_actionReportBug_triggered(); @@ -220,7 +222,7 @@ class MainWindow : public QMainWindow { void retranslateUi(); void addInstance(const QString& url = QString(), const QMap& extra_info = {}); - void activateInstance(InstancePtr instance); + void activateInstance(BaseInstance* instance); void setCatBackground(bool enabled); void updateInstanceToolIcon(QString new_icon); void setSelectedInstanceById(const QString& id); @@ -247,7 +249,7 @@ class MainWindow : public QMainWindow { unique_qobject_ptr m_newsChecker; - InstancePtr m_selectedInstance; + BaseInstance* m_selectedInstance = nullptr; QString m_currentInstIcon; // managed by the application object diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index ff1b4a25a..e9b9aa442 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -322,6 +322,14 @@ QAction::PreferencesRole + + + + + + Manage &Skins... + + @@ -520,7 +528,7 @@ Close the current window - QAction::QuitRole + QAction::MenuRole::NoRole @@ -721,7 +729,7 @@ - %1 &Help + %1 &Wiki Open the %1 wiki diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 6052e94a9..da42ae2b4 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -48,7 +48,7 @@ QString getCreditsHtml() { QFile dataFile(":/documents/credits.html"); if (!dataFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + qWarning() << "Failed to open file" << dataFile.fileName() << "for reading:" << dataFile.errorString(); return {}; } QString fileContent = QString::fromUtf8(dataFile.readAll()); @@ -66,7 +66,7 @@ QString getLicenseHtml() dataFile.close(); return output; } else { - qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + qWarning() << "Failed to open file" << dataFile.fileName() << "for reading:" << dataFile.errorString(); return QString(); } } diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 2a96b7f94..4abaf6eb5 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -27,6 +27,7 @@ #include "ui_BlockedModsDialog.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "modplatform/helpers/HashUtils.h" #include diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h index 51e7c98c6..3d602de93 100644 --- a/launcher/ui/dialogs/ChooseProviderDialog.h +++ b/launcher/ui/dialogs/ChooseProviderDialog.h @@ -2,13 +2,14 @@ #include #include +#include namespace Ui { class ChooseProviderDialog; } namespace ModPlatform { -enum class ResourceProvider; +enum class ResourceProvider : std::uint8_t; } class Mod; diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index e5c2c301b..74fab3407 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -51,7 +51,7 @@ #include "InstanceList.h" #include "icons/IconList.h" -CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) +CopyInstanceDialog::CopyInstanceDialog(BaseInstance* original, QWidget* parent) : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) { ui->setupUi(this); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.h b/launcher/ui/dialogs/CopyInstanceDialog.h index 698c6e939..5f150cf5f 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.h +++ b/launcher/ui/dialogs/CopyInstanceDialog.h @@ -30,7 +30,7 @@ class CopyInstanceDialog : public QDialog { Q_OBJECT public: - explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); + explicit CopyInstanceDialog(BaseInstance* original, QWidget* parent = 0); ~CopyInstanceDialog(); void updateDialogState(); @@ -71,7 +71,7 @@ class CopyInstanceDialog : public QDialog { /* data */ Ui::CopyInstanceDialog* ui; QString InstIconKey; - InstancePtr m_original; + BaseInstance* m_original; InstanceCopyPrefs m_selectedOptions; bool m_cloneSupported = false; bool m_linkSupported = false; diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 574881ad0..7d4199f13 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -55,7 +55,7 @@ #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" -CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) +CreateShortcutDialog::CreateShortcutDialog(BaseInstance* instance, QWidget* parent) : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) { ui->setupUi(this); @@ -64,7 +64,7 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setPlaceholderText(instance->name()); - auto mInst = std::dynamic_pointer_cast(instance); + auto mInst = dynamic_cast(instance); m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); auto worldList = mInst->worldList(); worldList->update(); @@ -212,7 +212,7 @@ void CreateShortcutDialog::createShortcut() if (ui->overrideAccountCheckbox->isChecked()) extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); - ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey, target }; + ShortcutUtils::Shortcut args{ m_instance, name, targetString, this, extraArgs, InstIconKey, target }; if (target == ShortcutTarget::Desktop) ShortcutUtils::createInstanceShortcutOnDesktop(args); else if (target == ShortcutTarget::Applications) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index 29e68a787..8d666ef63 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -28,7 +28,7 @@ class CreateShortcutDialog : public QDialog { Q_OBJECT public: - explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); + explicit CreateShortcutDialog(BaseInstance* instance, QWidget* parent = nullptr); ~CreateShortcutDialog(); void createShortcut(); @@ -51,7 +51,7 @@ class CreateShortcutDialog : public QDialog { // Data Ui::CreateShortcutDialog* ui; QString InstIconKey; - InstancePtr m_instance; + BaseInstance* m_instance; bool m_QuickJoinSupported = false; // Functions diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 21c16f01a..96dab97e3 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -60,7 +60,7 @@ #include "Application.h" #include "SeparatorPrefixTree.h" -ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) +ExportInstanceDialog::ExportInstanceDialog(BaseInstance* instance, QWidget* parent) : QDialog(parent), m_ui(new Ui::ExportInstanceDialog), m_instance(instance) { m_ui->setupUi(this); @@ -98,7 +98,7 @@ ExportInstanceDialog::~ExportInstanceDialog() } /// Save icon to instance's folder is needed -void SaveIcon(InstancePtr m_instance) +void SaveIcon(BaseInstance* m_instance) { auto iconKey = m_instance->iconKey(); auto iconList = APPLICATION->icons(); diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h index 989e1635a..c1f8559cc 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.h +++ b/launcher/ui/dialogs/ExportInstanceDialog.h @@ -43,7 +43,6 @@ #include "FileIgnoreProxy.h" class BaseInstance; -using InstancePtr = std::shared_ptr; namespace Ui { class ExportInstanceDialog; @@ -53,7 +52,7 @@ class ExportInstanceDialog : public QDialog { Q_OBJECT public: - explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0); + explicit ExportInstanceDialog(BaseInstance* instance, QWidget* parent = 0); ~ExportInstanceDialog(); virtual void done(int result); @@ -64,7 +63,7 @@ class ExportInstanceDialog : public QDialog { private: Ui::ExportInstanceDialog* m_ui; - InstancePtr m_instance; + BaseInstance* m_instance; FileIgnoreProxy* m_proxyModel; FastFileIconProvider m_icons; diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 17b3ba703..d0a9f0914 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -33,7 +33,7 @@ #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" -ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) +ExportPackDialog::ExportPackDialog(MinecraftInstance* instance, QWidget* parent, ModPlatform::ResourceProvider provider) : QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider) { Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); @@ -101,23 +101,20 @@ ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* paren const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); - MinecraftInstance* mcInstance = dynamic_cast(instance.get()); - if (mcInstance) { - for (auto resourceModel : mcInstance->resourceLists()) { - if (resourceModel == nullptr) { - continue; - } - - if (!resourceModel->indexDir().exists()) { - continue; - } - - if (resourceModel->dir() == resourceModel->indexDir()) { - continue; - } - - m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); + for (auto resourceModel : instance->resourceLists()) { + if (resourceModel == nullptr) { + continue; } + + if (!resourceModel->indexDir().exists()) { + continue; + } + + if (resourceModel->dir() == resourceModel->indexDir()) { + continue; + } + + m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); } m_ui->files->setModel(m_proxy); diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h index e93055d8d..81e657aa0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.h +++ b/launcher/ui/dialogs/ExportPackDialog.h @@ -33,7 +33,7 @@ class ExportPackDialog : public QDialog { Q_OBJECT public: - explicit ExportPackDialog(MinecraftInstancePtr instance, + explicit ExportPackDialog(MinecraftInstance* instance, QWidget* parent = nullptr, ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); ~ExportPackDialog(); @@ -45,7 +45,7 @@ class ExportPackDialog : public QDialog { QString ignoreFileName(); private: - const MinecraftInstancePtr m_instance; + MinecraftInstance* m_instance; Ui::ExportPackDialog* m_ui; FileIgnoreProxy* m_proxy; FastFileIconProvider m_icons; diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index e8873f9b4..a782a9190 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -214,6 +214,8 @@ void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) case ExportToModList::FileName: ui->templateText->insertPlainText("{filename}"); break; + case ExportToModList::None: + break; } } void ExportToModListDialog::enableCustom(bool enabled) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index c1b79a904..b2269cc70 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -30,26 +30,106 @@ #include "icons/IconList.h" #include "icons/IconUtils.h" +class IconProxyModel : public QSortFilterProxyModel +{ +public: + explicit IconProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) + { + } + + void setCategory(IconPickerDialog::IconPickerCategory category) + { + if (m_category == category) + return; + m_category = category; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override + { + if (!QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent)) + return false; + + if (m_category == IconPickerDialog::Any) + return true; + + auto model = static_cast(sourceModel()); + QModelIndex index = model->index(source_row, 0, source_parent); + QString key = model->data(index, Qt::UserRole).toString(); + const MMCIcon* icon = model->icon(key); + + if (!icon) + return false; + + bool isModpack = false; + bool isBuiltin = icon->isBuiltIn(); + bool isLegacy = isBuiltin && icon->name().endsWith("_legacy", Qt::CaseInsensitive); + + if (!isBuiltin) { + const QString& name = icon->name(); + if (name.startsWith("curseforge_", Qt::CaseInsensitive) || + name.startsWith("modrinth_", Qt::CaseInsensitive) || + name.startsWith("ftb_", Qt::CaseInsensitive) || + name.startsWith("technic_", Qt::CaseInsensitive) || + name.startsWith("atl_", Qt::CaseInsensitive)) { + isModpack = true; + } + } + + switch (m_category) { + case IconPickerDialog::Legacy: + return isBuiltin && isLegacy; + case IconPickerDialog::Modpacks: + return isModpack; + case IconPickerDialog::Modern: + return isBuiltin && !isLegacy; + case IconPickerDialog::Custom: + return !isBuiltin && !isModpack; + default: + return true; + } + } + +private: + IconPickerDialog::IconPickerCategory m_category = IconPickerDialog::Any; +}; + IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::IconPickerDialog) { ui->setupUi(this); setWindowModality(Qt::WindowModal); - searchBar = new QLineEdit(this); - searchBar->setPlaceholderText(tr("Search...")); - ui->verticalLayout->insertWidget(0, searchBar); + static const QString context_text[] = { + tr("All"), + tr("Modern"), + tr("Legacy"), + tr("Modpacks"), + tr("Custom"), + }; + static const IconPickerCategory context_id[] = { + Any, + Modern, + Legacy, + Modpacks, + Custom, + }; + const int cnt = sizeof(context_text) / sizeof(context_text[0]); + for (int i = 0; i < cnt; ++i) { + ui->contextCombo->addItem(context_text[i], context_id[i]); + if (i == 0) { + ui->contextCombo->insertSeparator(i + 1); + } + } - proxyModel = new QSortFilterProxyModel(this); - proxyModel->setSourceModel(APPLICATION->icons().get()); + proxyModel = new IconProxyModel(this); + proxyModel->setSourceModel(APPLICATION->icons()); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); ui->iconView->setModel(proxyModel); auto contentsWidget = ui->iconView; - contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); - contentsWidget->setMovement(QListView::Static); - contentsWidget->setResizeMode(QListView::Adjust); contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); contentsWidget->setSpacing(5); contentsWidget->setWordWrap(false); @@ -86,9 +166,13 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); - connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + connect(ui->searchLine, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + connect(ui->contextCombo, &QComboBox::currentIndexChanged, this, [this](int index) { + IconPickerCategory category = static_cast(ui->contextCombo->itemData(index).toInt()); + filterIconsByCategory(category); + }); // Prevent incorrect indices from e.g. filesystem changes - connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); + connect(APPLICATION->icons(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) @@ -182,3 +266,8 @@ void IconPickerDialog::filterIcons(const QString& query) { proxyModel->setFilterFixedString(query); } + +void IconPickerDialog::filterIconsByCategory(IconPickerCategory category) +{ + static_cast(proxyModel)->setCategory(category); +} diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index db1315338..063f9b905 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -32,6 +32,15 @@ class IconPickerDialog : public QDialog { int execWithSelection(QString selection); QString selectedIconKey; + enum IconPickerCategory { + Any, + Modern, + Legacy, + Modpacks, + Custom, + }; + Q_ENUM(IconPickerCategory) + protected: virtual bool eventFilter(QObject*, QEvent*); @@ -49,4 +58,5 @@ class IconPickerDialog : public QDialog { void removeSelectedIcon(); void openFolder(); void filterIcons(const QString& text); + void filterIconsByCategory(IconPickerCategory); }; diff --git a/launcher/ui/dialogs/IconPickerDialog.ui b/launcher/ui/dialogs/IconPickerDialog.ui index c548edfb7..948e7043f 100644 --- a/launcher/ui/dialogs/IconPickerDialog.ui +++ b/launcher/ui/dialogs/IconPickerDialog.ui @@ -15,7 +15,64 @@ - + + + + + + + Icon category + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Search Icons... + + + true + + + + + + + + + + + + 60 + 60 + + + + QListView::Static + + + QListView::Adjust + + + QListView::IconMode + + + true + + diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index 7cd178130..80096ed01 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -35,7 +35,7 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::Resou contentsWidget->setItemDelegate(new ListViewDelegate()); proxyModel = new InstanceProxyModel(this); - proxyModel->setSourceModel(APPLICATION->instances().get()); + proxyModel->setSourceModel(APPLICATION->instances()); proxyModel->sort(0); contentsWidget->setModel(proxyModel); diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index c811e01eb..deb5358fb 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -33,11 +33,7 @@ class InstallLoaderPage : public VersionSelectWidget, public BasePage { Q_OBJECT public: - InstallLoaderPage(const QString& id, - const QString& iconName, - const QString& name, - const Version& oldestVersion, - const std::shared_ptr profile) + InstallLoaderPage(const QString& id, const QString& iconName, const QString& name, const Version& oldestVersion, PackProfile* profile) : VersionSelectWidget(nullptr), uid(id), iconName(iconName), name(name) { const QString minecraftVersion = profile->getComponentVersion("net.minecraft"); @@ -88,25 +84,29 @@ static InstallLoaderPage* pageCast(BasePage* page) return result; } -InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, const QString& uid, QWidget* parent) - : QDialog(parent), profile(std::move(profile)), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) +InstallLoaderDialog::InstallLoaderDialog(PackProfile* profile, const QString& uid, QWidget* parent) + : QDialog(parent), profile(profile), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) { auto layout = new QVBoxLayout(this); + // small margins look ugly on macOS on modal windows + #ifndef Q_OS_MACOS layout->setContentsMargins(0, 0, 0, 0); - + #endif container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); + // small margins look ugly on macOS on modal windows + #ifndef Q_OS_MACOS buttonLayout->setContentsMargins(0, 0, 6, 6); - + #endif auto refreshButton = new QPushButton(tr("&Refresh"), this); - connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); + connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(true); }); buttonLayout->addWidget(refreshButton); buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); - buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + buttons->button(QDialogButtonBox::Ok)->setText(tr("OK")); buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); diff --git a/launcher/ui/dialogs/InstallLoaderDialog.h b/launcher/ui/dialogs/InstallLoaderDialog.h index 86cb3bdd2..501f136e2 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.h +++ b/launcher/ui/dialogs/InstallLoaderDialog.h @@ -30,7 +30,7 @@ class InstallLoaderDialog final : public QDialog, protected BasePageProvider { Q_OBJECT public: - explicit InstallLoaderDialog(std::shared_ptr instance, const QString& uid = QString(), QWidget* parent = nullptr); + explicit InstallLoaderDialog(PackProfile* instance, const QString& uid = QString(), QWidget* parent = nullptr); QList getPages() override; QString dialogTitle() override; @@ -39,7 +39,7 @@ class InstallLoaderDialog final : public QDialog, protected BasePageProvider { void done(int result) override; private: - std::shared_ptr profile; + PackProfile* profile; PageContainer* container; QDialogButtonBox* buttons; }; diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index e6b53d292..e238a54eb 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -35,6 +35,7 @@ #include "MSALoginDialog.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "ui_MSALoginDialog.h" diff --git a/launcher/ui/dialogs/NetworkJobFailedDialog.cpp b/launcher/ui/dialogs/NetworkJobFailedDialog.cpp new file mode 100644 index 000000000..e0d3a2c83 --- /dev/null +++ b/launcher/ui/dialogs/NetworkJobFailedDialog.cpp @@ -0,0 +1,86 @@ +// 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 . + */ + +#include "NetworkJobFailedDialog.h" + +#include +#include +#include +#include + +#include "ui_NetworkJobFailedDialog.h" + +NetworkJobFailedDialog::NetworkJobFailedDialog(const QString& jobName, const int attempts, const int requests, const int failed, QWidget* parent) + : QDialog(parent), m_ui(new Ui::NetworkJobFailedDialog) +{ + m_ui->setupUi(this); + m_ui->failLabel->setText(m_ui->failLabel->text().arg(jobName)); + if (failed == requests) { + m_ui->requestCountLabel->setText(tr("All %1 requests have failed after %2 attempts").arg(failed).arg(attempts)); + } else if (failed < requests / 2) { + m_ui->requestCountLabel->setText( + tr("Out of %1 requests, %2 have failed after %3 attempts").arg(requests).arg(failed).arg(attempts)); + } else { + m_ui->requestCountLabel->setText( + tr("Out of %1 requests, only %2 succeeded after %3 attempts").arg(requests).arg(requests - failed).arg(attempts)); + } + + m_ui->detailsTable->header()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->detailsTable->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + m_ui->detailsTable->setSelectionMode(QAbstractItemView::ExtendedSelection); + + const auto* copyShortcut = new QShortcut(QKeySequence::Copy, m_ui->detailsTable); + connect(copyShortcut, &QShortcut::activated, this, &NetworkJobFailedDialog::copyUrl); + + const auto* copyButton = m_ui->dialogButtonBox->addButton(tr("Copy URL"), QDialogButtonBox::ActionRole); + connect(copyButton, &QPushButton::clicked, this, &NetworkJobFailedDialog::copyUrl); + + connect(m_ui->dialogButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_ui->dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +NetworkJobFailedDialog::~NetworkJobFailedDialog() +{ + delete m_ui; +} + +void NetworkJobFailedDialog::addFailedRequest(const QUrl& url, QString error) const +{ + auto* item = new QTreeWidgetItem(m_ui->detailsTable, { url.toString(), std::move(error) }); + m_ui->detailsTable->addTopLevelItem(item); + if (m_ui->detailsTable->selectedItems().isEmpty()) { + m_ui->detailsTable->setCurrentItem(item); + } +} + +void NetworkJobFailedDialog::copyUrl() const +{ + auto items = m_ui->detailsTable->selectedItems(); + if (items.isEmpty()) { + return; + } + + QString urls = items.first()->text(0); + for (auto& item : items.sliced(1)) { + urls += "\n" + item->text(0); + } + + auto* clipboard = QGuiApplication::clipboard(); + clipboard->setText(urls); +} diff --git a/launcher/ui/dialogs/NetworkJobFailedDialog.h b/launcher/ui/dialogs/NetworkJobFailedDialog.h new file mode 100644 index 000000000..9bfb7c439 --- /dev/null +++ b/launcher/ui/dialogs/NetworkJobFailedDialog.h @@ -0,0 +1,43 @@ +// 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 + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { +class NetworkJobFailedDialog; +} +QT_END_NAMESPACE + +class NetworkJobFailedDialog : public QDialog { + Q_OBJECT + + public: + explicit NetworkJobFailedDialog(const QString& jobName, int attempts, int requests, int failed, QWidget* parent = nullptr); + ~NetworkJobFailedDialog() override; + + void addFailedRequest(const QUrl& url, QString error) const; + + private slots: + void copyUrl() const; + + private: + Ui::NetworkJobFailedDialog* m_ui; +}; diff --git a/launcher/ui/dialogs/NetworkJobFailedDialog.ui b/launcher/ui/dialogs/NetworkJobFailedDialog.ui new file mode 100644 index 000000000..b133052eb --- /dev/null +++ b/launcher/ui/dialogs/NetworkJobFailedDialog.ui @@ -0,0 +1,99 @@ + + + NetworkJobFailedDialog + + + + 0 + 0 + 450 + 350 + + + + Network error + + + + + + + + + + 0 + 0 + + + + A network operation has failed: %1 + + + + + + + + 0 + 0 + + + + (request count) + + + + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + true + + + QAbstractItemView::ScrollMode::ScrollPerPixel + + + 0 + + + false + + + + URL + + + + + Error + + + + + + + + + 0 + 0 + + + + What would you like to do? + + + + + + + QDialogButtonBox::StandardButton::Abort|QDialogButtonBox::StandardButton::Retry + + + + + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 8d80966ac..8cf094527 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -61,6 +61,7 @@ #include "ui/pages/modplatform/ImportPage.h" #include "ui/pages/modplatform/atlauncher/AtlPage.h" #include "ui/pages/modplatform/flame/FlamePage.h" +#include "ui/pages/modplatform/ftb/FtbPage.h" #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" @@ -177,6 +178,7 @@ QList NewInstanceDialog::getPages() pages.append(new AtlPage(this)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(new FlamePage(this)); + pages.append(new FtbPage(this)); pages.append(new LegacyFTB::Page(this)); pages.append(new FTBImportAPP::ImportFTBPage(this)); pages.append(new ModrinthPage(this)); diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp index b646e3918..0657c8967 100644 --- a/launcher/ui/dialogs/NewsDialog.cpp +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -1,4 +1,8 @@ #include "NewsDialog.h" + +#include "Application.h" +#include "settings/SettingsObject.h" + #include "ui_NewsDialog.h" NewsDialog::NewsDialog(QList entries, QWidget* parent) : QDialog(parent), ui(new Ui::NewsDialog()) @@ -23,6 +27,12 @@ NewsDialog::NewsDialog(QList entries, QWidget* parent) : QDialog(p ui->currentArticleContentBrowser->setText(article_entry->content); ui->currentArticleContentBrowser->flush(); + + connect(this, &QDialog::finished, this, [this] { + APPLICATION->settings()->set("NewsGeometry", QString::fromUtf8(saveGeometry().toBase64())); + }); + const QByteArray base64Geometry = APPLICATION->settings()->get("NewsGeometry").toString().toUtf8(); + restoreGeometry(QByteArray::fromBase64(base64Geometry)); } NewsDialog::~NewsDialog() diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index 90588ce05..4c4995fea 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -17,12 +17,26 @@ #include "ui_ProfileSelectDialog.h" #include +#include #include #include #include "Application.h" -#include "ui/dialogs/ProgressDialog.h" +// HACK: hide checkboxes from AccountList +class HideCheckboxProxyModel : public QIdentityProxyModel { + public: + using QIdentityProxyModel::QIdentityProxyModel; + + QVariant data(const QModelIndex& index, int role) const override + { + if (role == Qt::CheckStateRole) { + return {}; + } + + return QIdentityProxyModel::data(index, role); + } +}; ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWidget* parent) : QDialog(parent), ui(new Ui::ProfileSelectDialog) @@ -30,33 +44,10 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid ui->setupUi(this); m_accounts = APPLICATION->accounts(); - auto view = ui->listView; - // view->setModel(m_accounts.get()); - // view->hideColumn(AccountList::ActiveColumn); - view->setColumnCount(1); - view->setRootIsDecorated(false); - // FIXME: use a real model, not this - if (QTreeWidgetItem* header = view->headerItem()) { - header->setText(0, tr("Name")); - } else { - view->setHeaderLabel(tr("Name")); - } - QList items; - for (int i = 0; i < m_accounts->count(); i++) { - MinecraftAccountPtr account = m_accounts->at(i); - QString profileLabel; - if (account->isInUse()) { - profileLabel = tr("%1 (in use)").arg(account->profileName()); - } else { - profileLabel = account->profileName(); - } - auto item = new QTreeWidgetItem(view); - item->setText(0, profileLabel); - item->setIcon(0, account->getFace()); - item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); - items.append(item); - } - view->addTopLevelItems(items); + + auto proxy = new HideCheckboxProxyModel(ui->view); + proxy->setSourceModel(m_accounts); + ui->view->setModel(proxy); // Set the message label. ui->msgLabel->setVisible(!message.isEmpty()); @@ -68,9 +59,9 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid qDebug() << flags; // Select the first entry in the list. - ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); + ui->view->setCurrentIndex(ui->view->model()->index(0, 0)); - connect(ui->listView, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted); + connect(ui->view, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); @@ -98,7 +89,7 @@ bool ProfileSelectDialog::useAsInstDefaullt() const void ProfileSelectDialog::on_buttonBox_accepted() { - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + QModelIndexList selection = ui->view->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); m_selected = selected.data(AccountList::PointerRole).value(); diff --git a/launcher/ui/dialogs/ProfileSelectDialog.h b/launcher/ui/dialogs/ProfileSelectDialog.h index e56ba0527..a44e82d55 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.h +++ b/launcher/ui/dialogs/ProfileSelectDialog.h @@ -76,7 +76,7 @@ class ProfileSelectDialog : public QDialog { void on_buttonBox_rejected(); protected: - shared_qobject_ptr m_accounts; + AccountList* m_accounts; //! The account that was selected when the user clicked OK. MinecraftAccountPtr m_selected; diff --git a/launcher/ui/dialogs/ProfileSelectDialog.ui b/launcher/ui/dialogs/ProfileSelectDialog.ui index e779b51bf..a72b3e2e0 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.ui +++ b/launcher/ui/dialogs/ProfileSelectDialog.ui @@ -22,13 +22,7 @@ - - - - 1 - - - + @@ -51,7 +45,7 @@ - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index af8b26c66..291827b05 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -159,22 +159,23 @@ void ProfileSetupDialog::checkName(const QString& name) { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; - m_check_response.reset(new QByteArray()); if (m_check_task) disconnect(m_check_task.get(), nullptr, this, nullptr); - m_check_task = Net::Download::makeByteArray(url, m_check_response); - m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [task, response] = Net::Download::makeByteArray(url); - connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished); + m_check_task = task; + m_check_task->addHeaderProxy(std::make_unique(headers)); + + connect(m_check_task.get(), &Task::finished, this, [this, response] { checkFinished(response); }); m_check_task->setNetwork(APPLICATION->network()); m_check_task->start(); } -void ProfileSetupDialog::checkFinished() +void ProfileSetupDialog::checkFinished(QByteArray* response) { if (m_check_task->error() == QNetworkReply::NoError) { - auto doc = QJsonDocument::fromJson(*m_check_response); + auto doc = QJsonDocument::fromJson(*response); auto root = doc.object(); auto statusValue = root.value("status").toString("INVALID"); if (statusValue == "AVAILABLE") { @@ -205,11 +206,11 @@ void ProfileSetupDialog::setupProfile(const QString& profileName) { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; - m_profile_response.reset(new QByteArray()); - m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8()); - m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); + auto [task, response] = Net::Upload::makeByteArray(url, payloadTemplate.arg(profileName).toUtf8()); + m_profile_task = task; + m_profile_task->addHeaderProxy(std::make_unique(headers)); - connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished); + connect(m_profile_task.get(), &Task::finished, this, [this, response] { setupProfileFinished(response); }); m_profile_task->setNetwork(APPLICATION->network()); m_profile_task->start(); @@ -252,7 +253,7 @@ struct MojangError { } // namespace -void ProfileSetupDialog::setupProfileFinished() +void ProfileSetupDialog::setupProfileFinished(QByteArray* response) { isWorking = false; if (m_profile_task->error() == QNetworkReply::NoError) { @@ -262,7 +263,7 @@ void ProfileSetupDialog::setupProfileFinished() */ accept(); } else { - auto parsedError = MojangError::fromJSON(*m_profile_response); + auto parsedError = MojangError::fromJSON(*response); ui->errorLabel->setVisible(true); QString errorMessage = diff --git a/launcher/ui/dialogs/ProfileSetupDialog.h b/launcher/ui/dialogs/ProfileSetupDialog.h index c005a4138..1da9c1164 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.h +++ b/launcher/ui/dialogs/ProfileSetupDialog.h @@ -21,7 +21,6 @@ #include #include -#include #include "net/Download.h" #include "net/Upload.h" @@ -44,8 +43,8 @@ class ProfileSetupDialog : public QDialog { void nameEdited(const QString& name); void startCheck(); - void checkFinished(); - void setupProfileFinished(); + void checkFinished(QByteArray* response); + void setupProfileFinished(QByteArray* response); protected: void scheduleCheck(const QString& name); @@ -70,9 +69,6 @@ class ProfileSetupDialog : public QDialog { QTimer checkStartTimer; - std::shared_ptr m_check_response; Net::Download::Ptr m_check_task; - - std::shared_ptr m_profile_response; Net::Upload::Ptr m_profile_task; }; diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index aa2f67bdb..0a453bda5 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -152,6 +152,7 @@ int ProgressDialog::execWithTask(Task* task) this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); this->m_taskConnections.push_back(connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); + this->m_taskConnections.push_back(connect(task, &Task::abortButtonTextChanged, ui->skipButton, &QPushButton::setText)); m_is_multi_step = task->isMultiStep(); ui->taskProgressScrollArea->setHidden(!m_is_multi_step); @@ -251,10 +252,7 @@ void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress) task_bar->setValue(mapped_current); task_bar->setStatus(task_progress.status); task_bar->setDetails(task_progress.details); - - if (task_progress.isDone()) { - task_bar->setVisible(false); - } + task_bar->setVisible(!task_progress.isDone()); } void ProgressDialog::changeProgress(qint64 current, qint64 total) diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 2fd0b8c7c..bcb30c761 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "Application.h" #include "ResourceDownloadTask.h" @@ -49,11 +50,12 @@ namespace ResourceDownload { -ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model) +ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, ResourceFolderModel* baseModel, bool suppressInitialSearch) : QDialog(parent) - , m_base_model(base_model) + , m_base_model(baseModel) , m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel) , m_vertical_layout(this) + , m_suppressInitialSearch(suppressInitialSearch) { setObjectName(QStringLiteral("ResourceDownloadDialog")); @@ -61,32 +63,35 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share setWindowIcon(QIcon::fromTheme("new")); +// small margins look ugly on macOS on modal windows +#ifndef Q_OS_MACOS m_buttons.setContentsMargins(0, 0, 6, 6); - +#endif // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button - auto OkButton = m_buttons.button(QDialogButtonBox::Ok); - OkButton->setEnabled(false); - OkButton->setDefault(true); - OkButton->setAutoDefault(true); - OkButton->setText(tr("Review and confirm")); - OkButton->setShortcut(tr("Ctrl+Return")); + auto* okButton = m_buttons.button(QDialogButtonBox::Ok); + okButton->setEnabled(false); + okButton->setDefault(true); + okButton->setAutoDefault(true); + okButton->setText(tr("Review and confirm")); + okButton->setShortcut(tr("Ctrl+Return")); - auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); - CancelButton->setDefault(false); - CancelButton->setAutoDefault(false); + auto* cancelButton = m_buttons.button(QDialogButtonBox::Cancel); + cancelButton->setDefault(false); + cancelButton->setAutoDefault(false); - auto HelpButton = m_buttons.button(QDialogButtonBox::Help); - HelpButton->setDefault(false); - HelpButton->setAutoDefault(false); + auto* helpButton = m_buttons.button(QDialogButtonBox::Help); + helpButton->setDefault(false); + helpButton->setAutoDefault(false); setWindowModality(Qt::WindowModal); } void ResourceDownloadDialog::accept() { - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); + } QDialog::accept(); } @@ -106,8 +111,9 @@ void ResourceDownloadDialog::reject() } } - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); + } QDialog::reject(); } @@ -116,7 +122,10 @@ void ResourceDownloadDialog::reject() // won't work with subclasses if we put it in this ctor. void ResourceDownloadDialog::initializeContainer() { +// small margins look ugly on macOS on modal windows +#ifndef Q_OS_MACOS layout()->setContentsMargins(0, 0, 0, 0); +#endif m_container = new PageContainer(this, {}, this); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); @@ -130,28 +139,28 @@ void ResourceDownloadDialog::initializeContainer() void ResourceDownloadDialog::connectButtons() { - auto OkButton = m_buttons.button(QDialogButtonBox::Ok); - OkButton->setToolTip( + auto* okButton = m_buttons.button(QDialogButtonBox::Ok); + okButton->setToolTip( tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); - connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); + connect(okButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); - auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); - connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); + auto* cancelButton = m_buttons.button(QDialogButtonBox::Cancel); + connect(cancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); - auto HelpButton = m_buttons.button(QDialogButtonBox::Help); - connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + auto* helpButton = m_buttons.button(QDialogButtonBox::Help); + connect(helpButton, &QPushButton::clicked, m_container, &PageContainer::help); } void ResourceDownloadDialog::confirm() { - auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); - confirm_dialog->retranslateUi(resourcesString()); + auto* confirmDialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); + confirmDialog->retranslateUi(resourcesString()); QHash dependencyExtraInfo; QStringList depNames; if (auto task = getModDependenciesTask(); task) { connect(task.get(), &Task::failed, this, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); auto weak = task.toWeakRef(); connect(task.get(), &Task::succeeded, this, [this, weak]() { @@ -165,72 +174,77 @@ void ResourceDownloadDialog::confirm() }); // Check for updates - ProgressDialog progress_dialog(this); - progress_dialog.setSkipButton(true, tr("Abort")); - progress_dialog.setWindowTitle(tr("Checking for dependencies...")); - auto ret = progress_dialog.execWithTask(task.get()); + ProgressDialog progressDialog(this); + progressDialog.setSkipButton(true, tr("Abort")); + progressDialog.setWindowTitle(tr("Checking for dependencies...")); + auto ret = progressDialog.execWithTask(task.get()); // If the dialog was skipped / some download error happened if (ret == QDialog::DialogCode::Rejected) { QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; - } else { - for (auto dep : task->getDependecies()) { - addResource(dep->pack, dep->version); - depNames << dep->pack->name; - } - dependencyExtraInfo = task->getExtraInfo(); } + for (const auto& dep : task->getDependecies()) { + addResource(dep->pack, dep->version, "dependency"); + depNames << dep->pack->name; + } + dependencyExtraInfo = task->getExtraInfo(); } auto selected = getTasks(); - std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) { + std::ranges::sort(selected, [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) { return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; }); for (auto& task : selected) { auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString()); - confirm_dialog->appendResource({ task->getName(), task->getFilename(), ModPlatform::ProviderCapabilities::name(task->getProvider()), - extraInfo.required_by, task->getVersion().version_type.toString(), !extraInfo.maybe_installed }); + confirmDialog->appendResource({ .name = task->getName(), + .filename = task->getFilename(), + .provider = ModPlatform::ProviderCapabilities::name(task->getProvider()), + .required_by = extraInfo.required_by, + .version_type = task->getVersion().version_type.toString(), + .enabled = !extraInfo.maybe_installed }); } - if (confirm_dialog->exec()) { - auto deselected = confirm_dialog->deselectedResources(); - for (auto page : m_container->getPages()) { - auto res = static_cast(page); - for (auto name : deselected) + if (confirmDialog->exec() != 0) { + auto deselected = confirmDialog->deselectedResources(); + for (auto* page : m_container->getPages()) { + auto* res = static_cast(page); + for (const auto& name : deselected) { res->removeResourceFromPage(name); + } } this->accept(); } else { - for (auto name : depNames) + for (const auto& name : depNames) { removeResource(name); + } } } bool ResourceDownloadDialog::selectPage(QString pageId) { - return m_container->selectPage(pageId); + return m_container->selectPage(std::move(pageId)); } ResourcePage* ResourceDownloadDialog::selectedPage() { - ResourcePage* result = dynamic_cast(m_container->selectedPage()); + auto* result = dynamic_cast(m_container->selectedPage()); Q_ASSERT(result != nullptr); return result; } -void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver) +void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver, QString downloadReason) { removeResource(pack->name); - selectedPage()->addResourceToPage(pack, ver, getBaseModel()); + selectedPage()->addResourceToPage(pack, ver, getBaseModel(), std::move(downloadReason)); setButtonStatus(); } -void ResourceDownloadDialog::removeResource(const QString& pack_name) +void ResourceDownloadDialog::removeResource(const QString& packName) { - for (auto page : m_container->getPages()) { - static_cast(page)->removeResourceFromPage(pack_name); + for (auto* page : m_container->getPages()) { + static_cast(page)->removeResourceFromPage(packName); } setButtonStatus(); } @@ -238,18 +252,18 @@ void ResourceDownloadDialog::removeResource(const QString& pack_name) void ResourceDownloadDialog::setButtonStatus() { auto selected = false; - for (auto page : m_container->getPages()) { - auto res = static_cast(page); + for (auto* page : m_container->getPages()) { + auto* res = static_cast(page); selected = selected || res->hasSelectedPacks(); } m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected); } -const QList ResourceDownloadDialog::getTasks() +QList ResourceDownloadDialog::getTasks() { QList selected; - for (auto page : m_container->getPages()) { - auto res = static_cast(page); + for (auto* page : m_container->getPages()) { + auto* res = static_cast(page); selected.append(res->selectedPacks()); } return selected; @@ -257,28 +271,34 @@ const QList ResourceDownloadDialog::get void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) { - auto* prev_page = dynamic_cast(previous); - if (!prev_page) { + // If previous is null (first selection), nothing to sync + if (!previous) { + return; + } + + auto* prevPage = dynamic_cast(previous); + if (!prevPage) { qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; return; } // Same effect as having a global search bar - ResourcePage* result = dynamic_cast(selected); + auto* result = dynamic_cast(selected); Q_ASSERT(result != nullptr); - result->setSearchTerm(prev_page->getSearchTerm()); + result->setSearchTerm(prevPage->getSearchTerm()); } -ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) - : ResourceDownloadDialog(parent, mods), m_instance(instance) +ModDownloadDialog::ModDownloadDialog(QWidget* parent, ModFolderModel* mods, BaseInstance* instance, bool suppressInitialSearch) + : ResourceDownloadDialog(parent, mods, suppressInitialSearch), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } } QList ModDownloadDialog::getPages() @@ -287,10 +307,16 @@ QList ModDownloadDialog::getPages() auto loaders = static_cast(m_instance)->getPackProfile()->getSupportedModLoaders().value(); - if (ModrinthAPI::validateModLoaders(loaders)) - pages.append(ModrinthModPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) - pages.append(FlameModPage::create(this, *m_instance)); + if (ModrinthAPI::validateModLoaders(loaders)) { + auto* page = ModrinthModPage::create(this, *m_instance); + page->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(page); + } + if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) { + auto* page = FlameModPage::create(this, *m_instance); + page->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(page); + } return pages; } @@ -298,9 +324,9 @@ QList ModDownloadDialog::getPages() GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() { if (!APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - if (auto model = dynamic_cast(getBaseModel().get()); model) { + if (auto* model = dynamic_cast(getBaseModel()); model) { QList> selectedVers; - for (auto& selected : getTasks()) { + for (const auto& selected : getTasks()) { selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); } @@ -311,75 +337,96 @@ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() } ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance) - : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) + ResourcePackFolderModel* resourcePacks, + BaseInstance* instance, + bool suppressInitialSearch) + : ResourceDownloadDialog(parent, resourcePacks, suppressInitialSearch), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } } QList ResourcePackDownloadDialog::getPages() { QList pages; - pages.append(ModrinthResourcePackPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameResourcePackPage::create(this, *m_instance)); + auto* modrinthPage = ModrinthResourcePackPage::create(this, *m_instance); + modrinthPage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(modrinthPage); + if (APPLICATION->capabilities() & Application::SupportsFlame) { + auto* flamePage = FlameResourcePackPage::create(this, *m_instance); + flamePage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(flamePage); + } return pages; } TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance) - : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) + TexturePackFolderModel* resourcePacks, + BaseInstance* instance, + bool suppressInitialSearch) + : ResourceDownloadDialog(parent, resourcePacks, suppressInitialSearch), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } } QList TexturePackDownloadDialog::getPages() { QList pages; - pages.append(ModrinthTexturePackPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameTexturePackPage::create(this, *m_instance)); + auto* modrinthPage = ModrinthTexturePackPage::create(this, *m_instance); + modrinthPage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(modrinthPage); + if (APPLICATION->capabilities() & Application::SupportsFlame) { + auto* flamePage = FlameTexturePackPage::create(this, *m_instance); + flamePage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(flamePage); + } return pages; } ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, - const std::shared_ptr& shaders, - BaseInstance* instance) - : ResourceDownloadDialog(parent, shaders), m_instance(instance) + ShaderPackFolderModel* shaders, + BaseInstance* instance, + bool suppressInitialSearch) + : ResourceDownloadDialog(parent, shaders, suppressInitialSearch), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); + } } QList ShaderPackDownloadDialog::getPages() { QList pages; - pages.append(ModrinthShaderPackPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameShaderPackPage::create(this, *m_instance)); + auto* modrinthPage = ModrinthShaderPackPage::create(this, *m_instance); + modrinthPage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(modrinthPage); + if (APPLICATION->capabilities() & Application::SupportsFlame) { + auto* flamePage = FlameShaderPackPage::create(this, *m_instance); + flamePage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(flamePage); + } return pages; } @@ -393,33 +440,41 @@ void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptrname)); m_container->hidePageList(); m_buttons.hide(); - auto page = selectedPage(); + auto* page = selectedPage(); page->openProject(meta->project_id); } DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, - const std::shared_ptr& data_packs, - BaseInstance* instance) - : ResourceDownloadDialog(parent, data_packs), m_instance(instance) + DataPackFolderModel* dataPacks, + BaseInstance* instance, + bool suppressInitialSearch) + : ResourceDownloadDialog(parent, dataPacks, suppressInitialSearch), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); - if (!geometrySaveKey().isEmpty()) + if (!geometrySaveKey().isEmpty()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + } } QList DataPackDownloadDialog::getPages() { QList pages; - pages.append(ModrinthDataPackPage::create(this, *m_instance)); - if (APPLICATION->capabilities() & Application::SupportsFlame) - pages.append(FlameDataPackPage::create(this, *m_instance)); + auto* modrinthPage = ModrinthDataPackPage::create(this, *m_instance); + modrinthPage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(modrinthPage); + if (APPLICATION->capabilities() & Application::SupportsFlame) { + auto* flamePage = FlameDataPackPage::create(this, *m_instance); + flamePage->setSuppressInitialSearch(m_suppressInitialSearch); + pages.append(flamePage); + } return pages; } diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index a83f3c536..bea6c7689 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -51,7 +51,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { public: using DownloadTaskPtr = shared_qobject_ptr; - ResourceDownloadDialog(QWidget* parent, std::shared_ptr base_model); + ResourceDownloadDialog(QWidget* parent, ResourceFolderModel* baseModel, bool suppressInitialSearch = false); void initializeContainer(); void connectButtons(); @@ -64,11 +64,11 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { bool selectPage(QString pageId); ResourcePage* selectedPage(); - void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); + void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, QString downloadReason = "standalone"); void removeResource(const QString&); - const QList getTasks(); - const std::shared_ptr getBaseModel() const { return m_base_model; } + QList getTasks(); + ResourceFolderModel* getBaseModel() const { return m_base_model; } void setResourceMetadata(const std::shared_ptr& meta); @@ -88,19 +88,22 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } protected: - const std::shared_ptr m_base_model; + ResourceFolderModel* m_base_model; PageContainer* m_container = nullptr; QDialogButtonBox m_buttons; QVBoxLayout m_vertical_layout; + + protected: + bool m_suppressInitialSearch = false; }; class ModDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance); + explicit ModDownloadDialog(QWidget* parent, ModFolderModel* mods, BaseInstance* instance, bool suppressInitialSearch = false); ~ModDownloadDialog() override = default; //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) @@ -119,8 +122,9 @@ class ResourcePackDownloadDialog final : public ResourceDownloadDialog { public: explicit ResourcePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance); + ResourcePackFolderModel* resourcePacks, + BaseInstance* instance, + bool suppressInitialSearch = false); ~ResourcePackDownloadDialog() override = default; //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) @@ -138,8 +142,9 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog { public: explicit TexturePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance); + TexturePackFolderModel* resourcePacks, + BaseInstance* instance, + bool suppressInitialSearch = false); ~TexturePackDownloadDialog() override = default; //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) @@ -156,7 +161,10 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr& shader_packs, BaseInstance* instance); + explicit ShaderPackDownloadDialog(QWidget* parent, + ShaderPackFolderModel* shaders, + BaseInstance* instance, + bool suppressInitialSearch = false); ~ShaderPackDownloadDialog() override = default; //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) @@ -173,7 +181,10 @@ class DataPackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit DataPackDownloadDialog(QWidget* parent, const std::shared_ptr& data_packs, BaseInstance* instance); + explicit DataPackDownloadDialog(QWidget* parent, + DataPackFolderModel* dataPacks, + BaseInstance* instance, + bool suppressInitialSearch = false); ~DataPackDownloadDialog() override = default; //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp index 494c17e3f..e9776c660 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -29,14 +29,16 @@ #include -static std::list mcVersions(BaseInstance* inst) +namespace { +std::vector mcVersions(BaseInstance* inst) { return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } +} // namespace ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, - const std::shared_ptr resourceModel, + ResourceFolderModel* resourceModel, QList& searchFor, bool includeDeps, QList loadersList) @@ -58,8 +60,8 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, void ResourceUpdateDialog::checkCandidates() { // Ensure mods have valid metadata - auto went_well = ensureMetadata(); - if (!went_well) { + auto wentWell = ensureMetadata(); + if (!wentWell) { m_aborted = true; return; } @@ -73,12 +75,12 @@ void ResourceUpdateDialog::checkCandidates() text += tr("Mod name: %1
File name: %2
Reason: %3

").arg(mod->name(), mod->fileinfo().fileName(), reason); } - ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), - tr("Could not generate metadata for the following resources:
" - "Do you wish to proceed without those resources?"), - text); - message_dialog.setModal(true); - if (message_dialog.exec() == QDialog::Rejected) { + ScrollMessageBox messageDialog(m_parent, tr("Metadata generation failed"), + tr("Could not generate metadata for the following resources:
" + "Do you wish to proceed without those resources?"), + text); + messageDialog.setModal(true); + if (messageDialog.exec() == QDialog::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; @@ -87,40 +89,41 @@ void ResourceUpdateDialog::checkCandidates() auto versions = mcVersions(m_instance); - SequentialTask check_task(tr("Checking for updates")); + SequentialTask checkTask(tr("Checking for updates")); if (!m_modrinthToUpdate.empty()) { m_modrinthCheckTask.reset(new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel)); connect(m_modrinthCheckTask.get(), &CheckUpdateTask::checkFailed, this, - [this](Resource* resource, QString reason, QUrl recover_url) { - m_failedCheckUpdate.append({ resource, reason, recover_url }); + [this](Resource* resource, const QString& reason, const QUrl& recoverUrl) { + m_failedCheckUpdate.append({ resource, reason, recoverUrl }); }); - check_task.addTask(m_modrinthCheckTask); + checkTask.addTask(m_modrinthCheckTask); } if (!m_flameToUpdate.empty()) { m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel)); - connect(m_flameCheckTask.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { - m_failedCheckUpdate.append({ resource, reason, recover_url }); - }); - check_task.addTask(m_flameCheckTask); + connect(m_flameCheckTask.get(), &CheckUpdateTask::checkFailed, this, + [this](Resource* resource, const QString& reason, const QUrl& recoverUrl) { + m_failedCheckUpdate.append({ resource, reason, recoverUrl }); + }); + checkTask.addTask(m_flameCheckTask); } - connect(&check_task, &Task::failed, this, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + connect(&checkTask, &Task::failed, this, + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - connect(&check_task, &Task::succeeded, this, [this, &check_task]() { - QStringList warnings = check_task.warnings(); + connect(&checkTask, &Task::succeeded, this, [this, &checkTask]() { + QStringList warnings = checkTask.warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } }); // Check for updates - ProgressDialog progress_dialog(m_parent); - progress_dialog.setSkipButton(true, tr("Abort")); - progress_dialog.setWindowTitle(tr("Checking for updates...")); - auto ret = progress_dialog.execWithTask(&check_task); + ProgressDialog progressDialog(m_parent); + progressDialog.setSkipButton(true, tr("Abort")); + progressDialog.setWindowTitle(tr("Checking for updates...")); + auto ret = progressDialog.execWithTask(&checkTask); // If the dialog was skipped / some download error happened if (ret == QDialog::DialogCode::Rejected) { @@ -133,8 +136,8 @@ void ResourceUpdateDialog::checkCandidates() // Add found updates for Modrinth if (m_modrinthCheckTask) { - auto modrinth_updates = m_modrinthCheckTask->getUpdates(); - for (auto& updatable : modrinth_updates) { + auto modrinthUpdates = m_modrinthCheckTask->getUpdates(); + for (auto& updatable : modrinthUpdates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); @@ -145,8 +148,8 @@ void ResourceUpdateDialog::checkCandidates() // Add found updated for Flame if (m_flameCheckTask) { - auto flame_updates = m_flameCheckTask->getUpdates(); - for (auto& updatable : flame_updates) { + auto flameUpdates = m_flameCheckTask->getUpdates(); + for (auto& updatable : flameUpdates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); @@ -161,37 +164,47 @@ void ResourceUpdateDialog::checkCandidates() for (const auto& failed : m_failedCheckUpdate) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); - const auto& recover_url = std::get<2>(failed); + const auto& recoverUrl = std::get<2>(failed); qDebug() << mod->name() << "failed to check for updates!"; text += tr("Mod name: %1").arg(mod->name()) + "
"; - if (!reason.isEmpty()) + if (!reason.isEmpty()) { text += tr("Reason: %1").arg(reason) + "
"; - if (!recover_url.isEmpty()) + } + if (!recoverUrl.isEmpty()) { //: %1 is the link to download it manually text += tr("Possible solution: Getting the latest version manually:
%1
") - .arg(QString("%1").arg(recover_url.toString())); + .arg(QString("%1").arg(recoverUrl.toString())); + } text += "
"; } - ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), - tr("Could not check or get the following resources for updates:
" - "Do you wish to proceed without those resources?"), - text); - message_dialog.setModal(true); - if (message_dialog.exec() == QDialog::Rejected) { + ScrollMessageBox messageDialog(m_parent, tr("Failed to check for updates"), + tr("Could not check or get the following resources for updates:
" + "Do you wish to proceed without those resources?"), + text, "Disable unavailable mods"); + messageDialog.setModal(true); + if (messageDialog.exec() == QDialog::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } + + // Disable unavailable mods + if (messageDialog.isOptionChecked()) { + for (const auto& failed : m_failedCheckUpdate) { + const auto& mod = std::get<0>(failed); + mod->enable(EnableAction::DISABLE); + } + } } if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - auto* mod_model = dynamic_cast(m_resourceModel.get()); + auto* modModel = dynamic_cast(m_resourceModel); - if (mod_model != nullptr) { - auto depTask = makeShared(m_instance, mod_model, selectedVers); + if (modModel != nullptr) { + auto depTask = makeShared(m_instance, modModel, selectedVers); connect(depTask.get(), &Task::failed, this, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); @@ -207,10 +220,10 @@ void ResourceUpdateDialog::checkCandidates() } }); - ProgressDialog progress_dialog_deps(m_parent); - progress_dialog_deps.setSkipButton(true, tr("Abort")); - progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); - auto dret = progress_dialog_deps.execWithTask(depTask.get()); + ProgressDialog progressDialogDeps(m_parent); + progressDialogDeps.setSkipButton(true, tr("Abort")); + progressDialogDeps.setWindowTitle(tr("Checking for dependencies...")); + auto dret = progressDialogDeps.execWithTask(depTask.get()); // If the dialog was skipped / some download error happened if (dret == QDialog::DialogCode::Rejected) { @@ -218,19 +231,20 @@ void ResourceUpdateDialog::checkCandidates() QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } - static FlameAPI api; + static FlameAPI s_api; auto dependencyExtraInfo = depTask->getExtraInfo(); for (const auto& dep : depTask->getDependecies()) { auto changelog = dep->version.changelog; - if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) - changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); - auto download_task = makeShared(dep->pack, dep->version, m_resourceModel); + if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) { + changelog = s_api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); + } + auto downloadTask = makeShared(dep->pack, dep->version, m_resourceModel, true, "dependency"); auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); CheckUpdateTask::Update updatable = { dep->pack->name, dep->version.hash, tr("Not installed"), dep->version.version, dep->version.version_type, - changelog, dep->pack->provider, download_task, !extraInfo.maybe_installed + changelog, dep->pack->provider, downloadTask, !extraInfo.maybe_installed }; appendResource(updatable, extraInfo.required_by); @@ -256,56 +270,58 @@ void ResourceUpdateDialog::checkCandidates() } } - if (m_aborted || m_noUpdates) + if (m_aborted || m_noUpdates) { QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + } } // Part 1: Ensure we have a valid metadata auto ResourceUpdateDialog::ensureMetadata() -> bool { - auto index_dir = indexDir(); + auto indexDir2 = indexDir(); SequentialTask seq(tr("Looking for metadata")); // A better use of data structures here could remove the need for this QHash - QHash should_try_others; - QList modrinth_tmp; - QList flame_tmp; + QHash shouldTryOthers; + QList modrinthTmp; + QList flameTmp; - bool confirm_rest = false; - bool try_others_rest = false; - bool skip_rest = false; - ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; + bool confirmRest = false; + bool tryOthersRest = false; + bool skipRest = false; + ModPlatform::ResourceProvider providerRest = ModPlatform::ResourceProvider::MODRINTH; // adds resource to list based on provider - auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) { + auto addToTmp = [&modrinthTmp, &flameTmp](Resource* resource, ModPlatform::ResourceProvider p) { switch (p) { case ModPlatform::ResourceProvider::MODRINTH: - modrinth_tmp.push_back(resource); + modrinthTmp.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - flame_tmp.push_back(resource); + flameTmp.push_back(resource); break; } }; // ask the user on what provider to seach for the mod first - for (auto candidate : m_candidates) { - if (candidate->status() != ResourceStatus::NO_METADATA) { + for (auto* candidate : m_candidates) { + if (candidate->status() != ResourceStatus::NoMetadata) { onMetadataEnsured(candidate); continue; } - if (skip_rest) + if (skipRest) { continue; + } if (candidate->type() == ResourceType::FOLDER) { continue; } - if (confirm_rest) { - addToTmp(candidate, provider_rest); - should_try_others.insert(candidate->internal_id(), try_others_rest); + if (confirmRest) { + addToTmp(candidate, providerRest); + shouldTryOthers.insert(candidate->internalId(), tryOthersRest); continue; } @@ -318,68 +334,73 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool auto response = chooser.getResponse(); - if (response.skip_all) - skip_rest = true; + if (response.skip_all) { + skipRest = true; + } if (response.confirm_all) { - confirm_rest = true; - provider_rest = response.chosen; - try_others_rest = response.try_others; + confirmRest = true; + providerRest = response.chosen; + tryOthersRest = response.try_others; } - should_try_others.insert(candidate->internal_id(), response.try_others); + shouldTryOthers.insert(candidate->internalId(), response.try_others); - if (confirmed) + if (confirmed) { addToTmp(candidate, response.chosen); + } } // prepare task for the modrinth mods - if (!modrinth_tmp.empty()) { - auto modrinth_task = makeShared(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); + if (!modrinthTmp.empty()) { + auto modrinthTask = makeShared(modrinthTmp, indexDir2, ModPlatform::ResourceProvider::MODRINTH); + connect(modrinthTask.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(modrinthTask.get(), &EnsureMetadataTask::metadataFailed, [this, &shouldTryOthers](Resource* candidate) { + onMetadataFailed(candidate, shouldTryOthers.find(candidate->internalId()).value(), ModPlatform::ResourceProvider::MODRINTH); }); - connect(modrinth_task.get(), &EnsureMetadataTask::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + connect(modrinthTask.get(), &EnsureMetadataTask::failed, + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - if (modrinth_task->getHashingTask()) - seq.addTask(modrinth_task->getHashingTask()); + if (modrinthTask->getHashingTask()) { + seq.addTask(modrinthTask->getHashingTask()); + } - seq.addTask(modrinth_task); + seq.addTask(modrinthTask); } // prepare task for the flame mods - if (!flame_tmp.empty()) { - auto flame_task = makeShared(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); - connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); - connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); + if (!flameTmp.empty()) { + auto flameTask = makeShared(flameTmp, indexDir2, ModPlatform::ResourceProvider::FLAME); + connect(flameTask.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(flameTask.get(), &EnsureMetadataTask::metadataFailed, [this, &shouldTryOthers](Resource* candidate) { + onMetadataFailed(candidate, shouldTryOthers.find(candidate->internalId()).value(), ModPlatform::ResourceProvider::FLAME); }); - connect(flame_task.get(), &EnsureMetadataTask::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + connect(flameTask.get(), &EnsureMetadataTask::failed, + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - if (flame_task->getHashingTask()) - seq.addTask(flame_task->getHashingTask()); + if (flameTask->getHashingTask()) { + seq.addTask(flameTask->getHashingTask()); + } - seq.addTask(flame_task); + seq.addTask(flameTask); } seq.addTask(m_secondTryMetadata); // execute all the tasks - ProgressDialog checking_dialog(m_parent); - checking_dialog.setSkipButton(true, tr("Abort")); - checking_dialog.setWindowTitle(tr("Generating metadata...")); - auto ret_metadata = checking_dialog.execWithTask(&seq); + ProgressDialog checkingDialog(m_parent); + checkingDialog.setSkipButton(true, tr("Abort")); + checkingDialog.setWindowTitle(tr("Generating metadata...")); + auto retMetadata = checkingDialog.execWithTask(&seq); - return (ret_metadata != QDialog::DialogCode::Rejected); + return (retMetadata != QDialog::DialogCode::Rejected); } void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) { // When the mod is a folder, for instance - if (!resource->metadata()) + if (!resource->metadata()) { return; + } switch (resource->metadata()->provider) { case ModPlatform::ResourceProvider::MODRINTH: @@ -403,12 +424,12 @@ ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) return ModPlatform::ResourceProvider::FLAME; } -void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, ModPlatform::ResourceProvider first_choice) +void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool tryOthers, ModPlatform::ResourceProvider firstChoice) { - if (try_others) { - auto index_dir = indexDir(); + if (tryOthers) { + auto indexDir2 = indexDir(); - auto task = makeShared(resource, index_dir, next(first_choice)); + auto task = makeShared(resource, indexDir2, next(firstChoice)); connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); }); connect(task.get(), &EnsureMetadataTask::failed, @@ -428,57 +449,57 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, } } -void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy) +void ResourceUpdateDialog::appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy) { - auto item_top = new QTreeWidgetItem(ui->modTreeWidget); - item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + auto* itemTop = new QTreeWidgetItem(ui->modTreeWidget); + itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); if (!info.enabled) { - item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); + itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } - item_top->setText(0, info.name); - item_top->setExpanded(true); + itemTop->setText(0, info.name); + itemTop->setExpanded(true); - auto provider_item = new QTreeWidgetItem(item_top); - QString provider_name = ModPlatform::ProviderCapabilities::readableName(info.provider); - provider_item->setText(0, tr("Provider: %1").arg(provider_name)); - provider_item->setData(0, Qt::UserRole, provider_name); + auto* providerItem = new QTreeWidgetItem(itemTop); + QString providerName = ModPlatform::ProviderCapabilities::readableName(info.provider); + providerItem->setText(0, tr("Provider: %1").arg(providerName)); + providerItem->setData(0, Qt::UserRole, providerName); - auto old_version_item = new QTreeWidgetItem(item_top); - old_version_item->setText(0, tr("Old version: %1").arg(info.old_version)); - old_version_item->setData(0, Qt::UserRole, info.old_version); + auto* oldVersionItem = new QTreeWidgetItem(itemTop); + oldVersionItem->setText(0, tr("Old version: %1").arg(info.oldVersion)); + oldVersionItem->setData(0, Qt::UserRole, info.oldVersion); - auto new_version_item = new QTreeWidgetItem(item_top); - new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); - new_version_item->setData(0, Qt::UserRole, info.new_version); + auto* newVersionItem = new QTreeWidgetItem(itemTop); + newVersionItem->setText(0, tr("New version: %1").arg(info.newVersion)); + newVersionItem->setData(0, Qt::UserRole, info.newVersion); - if (info.new_version_type.has_value()) { - auto new_version_type_item = new QTreeWidgetItem(item_top); - new_version_type_item->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); - new_version_type_item->setData(0, Qt::UserRole, info.new_version_type.value().toString()); + if (info.newVersionType.has_value()) { + auto* newVersionTypeItem = new QTreeWidgetItem(itemTop); + newVersionTypeItem->setText(0, tr("New Version Type: %1").arg(info.newVersionType.value().toString())); + newVersionTypeItem->setData(0, Qt::UserRole, info.newVersionType.value().toString()); } if (!requiredBy.isEmpty()) { - auto requiredByItem = new QTreeWidgetItem(item_top); + auto* requiredByItem = new QTreeWidgetItem(itemTop); if (requiredBy.length() == 1) { requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back())); requiredByItem->setData(0, Qt::UserRole, requiredBy.back()); } else { requiredByItem->setText(0, tr("Required by:")); - for (auto req : requiredBy) { - auto reqItem = new QTreeWidgetItem(requiredByItem); + for (const auto& req : requiredBy) { + auto* reqItem = new QTreeWidgetItem(requiredByItem); reqItem->setText(0, req); } } ui->toggleDepsButton->show(); - m_deps << item_top; + m_deps << itemTop; } - auto changelog_item = new QTreeWidgetItem(item_top); - changelog_item->setText(0, tr("Changelog of the latest version")); + auto* changelogItem = new QTreeWidgetItem(itemTop); + changelogItem->setText(0, tr("Changelog of the latest version")); - auto changelog = new QTreeWidgetItem(changelog_item); - auto changelog_area = new QTextBrowser(); + auto* changelog = new QTreeWidgetItem(changelogItem); + auto* changelogArea = new QTextBrowser(); QString text = info.changelog; changelog->setData(0, Qt::UserRole, text); @@ -486,14 +507,14 @@ void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, Q text = markdownToHTML(info.changelog.toUtf8()); } - changelog_area->setHtml(StringUtils::htmlListPatch(text)); - changelog_area->setOpenExternalLinks(true); - changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); - changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + changelogArea->setHtml(StringUtils::htmlListPatch(text)); + changelogArea->setOpenExternalLinks(true); + changelogArea->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + changelogArea->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); - ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); + ui->modTreeWidget->setItemWidget(changelog, 0, changelogArea); - ui->modTreeWidget->addTopLevelItem(item_top); + ui->modTreeWidget->addTopLevelItem(itemTop); } auto ResourceUpdateDialog::getTasks() -> const QList diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h index be3c19dcc..9de1ee246 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.h +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -18,7 +18,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox { public: explicit ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, - std::shared_ptr resourceModel, + ResourceFolderModel* resourceModel, QList& searchFor, bool includeDeps, QList loadersList = {}); @@ -39,7 +39,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox { private slots: void onMetadataEnsured(Resource* resource); void onMetadataFailed(Resource* resource, - bool try_others = false, + bool tryOthers = false, ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH); private: @@ -48,7 +48,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox { shared_qobject_ptr m_modrinthCheckTask; shared_qobject_ptr m_flameCheckTask; - const std::shared_ptr m_resourceModel; + ResourceFolderModel* m_resourceModel; QList& m_candidates; QList m_modrinthToUpdate; diff --git a/launcher/ui/dialogs/ScrollMessageBox.cpp b/launcher/ui/dialogs/ScrollMessageBox.cpp index 1cfb848f4..361b610be 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.cpp +++ b/launcher/ui/dialogs/ScrollMessageBox.cpp @@ -2,7 +2,7 @@ #include #include "ui_ScrollMessageBox.h" -ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) +ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body, const QString& option) : QDialog(parent), ui(new Ui::ScrollMessageBox) { ui->setupUi(this); @@ -10,6 +10,11 @@ ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const ui->label->setText(text); ui->textBrowser->setText(body); + if (!option.isEmpty()) { + ui->optionCheckBox->setVisible(true); + ui->optionCheckBox->setText(option); + } + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } @@ -18,3 +23,8 @@ ScrollMessageBox::~ScrollMessageBox() { delete ui; } + +bool ScrollMessageBox::isOptionChecked() const +{ + return ui->optionCheckBox->isChecked(); +} diff --git a/launcher/ui/dialogs/ScrollMessageBox.h b/launcher/ui/dialogs/ScrollMessageBox.h index 8fd6769c4..f91c90273 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.h +++ b/launcher/ui/dialogs/ScrollMessageBox.h @@ -12,10 +12,12 @@ class ScrollMessageBox : public QDialog { Q_OBJECT public: - ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body); + ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body, const QString& option = {}); ~ScrollMessageBox() override; + bool isOptionChecked() const; + private: Ui::ScrollMessageBox* ui; }; diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui index e684185f2..2ebe86074 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.ui +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -25,14 +25,25 @@ - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - + + + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + @@ -81,4 +92,4 @@ - + \ No newline at end of file diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index 30377288b..db9b08096 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -69,7 +69,7 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); - m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_horizontalLayout->addWidget(m_buttonBox); @@ -144,7 +144,7 @@ BaseVersion::Ptr VersionSelectDialog::selectedVersion() const void VersionSelectDialog::on_refreshButton_clicked() { - m_versionWidget->loadList(); + m_versionWidget->loadList(true); } void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 3416c0b2d..4dfeeaa34 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -28,11 +28,13 @@ #include #include #include +#include #include #include #include #include "Application.h" +#include "settings/SettingsObject.h" #include "DesktopServices.h" #include "Json.h" #include "QObjectPtr.h" @@ -470,14 +472,11 @@ void SkinManageDialog::on_userBtn_clicked() NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; job->setAskRetry(false); - auto uuidOut = std::make_shared(); - auto profileOut = std::make_shared(); - auto uuidLoop = makeShared(); auto profileLoop = makeShared(); - auto getUUID = Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user, uuidOut); - auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); + auto [getUUID, uuidOut] = Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user); + auto [getProfile, profileOut] = Net::Download::makeByteArray(QUrl()); auto downloadSkin = Net::Download::makeFile(QUrl(), path); QString failReason; diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp index 4404442a4..f1f771303 100644 --- a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -247,11 +247,11 @@ void BoxGeometry::initGeometry(float u, float v, float width, float height, floa // Transfer vertex data to VBO 0 m_vertexBuf.bind(); - m_vertexBuf.allocate(verticesData.constData(), verticesData.size() * sizeof(VertexData)); + m_vertexBuf.allocate(verticesData.constData(), static_cast(verticesData.size() * sizeof(VertexData))); // Transfer index data to VBO 1 m_indexBuf.bind(); - m_indexBuf.allocate(indices.constData(), indices.size() * sizeof(GLushort)); + m_indexBuf.allocate(indices.constData(), static_cast(indices.size() * sizeof(GLushort))); m_indecesCount = indices.size(); } @@ -266,11 +266,11 @@ BoxGeometry* BoxGeometry::Plane() // Transfer vertex data to VBO 0 b->m_vertexBuf.bind(); - b->m_vertexBuf.allocate(planeVertices.constData(), planeVertices.size() * sizeof(VertexData)); + b->m_vertexBuf.allocate(planeVertices.constData(), static_cast(planeVertices.size() * sizeof(VertexData))); // Transfer index data to VBO 1 b->m_indexBuf.bind(); - b->m_indexBuf.allocate(planeIndices.constData(), planeIndices.size() * sizeof(GLushort)); + b->m_indexBuf.allocate(planeIndices.constData(), static_cast(planeIndices.size() * sizeof(GLushort))); b->m_indecesCount = planeIndices.size(); return b; diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp index 7e84a3a49..1d06c694f 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.cpp +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -34,9 +34,9 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctio // body new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), // right leg - new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9, -18, -0.1), QPoint(0, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9f, -18, -0.1f), QPoint(0, 16), QVector3D(4, 12, 4)), // left leg - new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9f, -18, -0.1f), QPoint(16, 48), QVector3D(4, 12, 4)), }; m_staticComponentsOverlay = { @@ -45,9 +45,9 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctio // body new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), // right leg - new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5f, 12.5f, 4.5f), QVector3D(-1.9f, -18, -0.1f), QPoint(0, 32), QVector3D(4, 12, 4)), // left leg - new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(1.9, -18, -0.1), QPoint(0, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5f, 12.5f, 4.5f), QVector3D(1.9f, -18, -0.1f), QPoint(0, 48), QVector3D(4, 12, 4)), }; m_normalArms = { @@ -79,7 +79,7 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctio }; m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), QVector3D(0, -8, 2.5), QPoint(0, 0), QVector3D(10, 16, 1), QSize(64, 32)); - m_cape->rotate(10.8, QVector3D(1, 0, 0)); + m_cape->rotate(10.8f, QVector3D(1, 0, 0)); m_cape->rotate(180, QVector3D(0, 1, 0)); auto leftWing = @@ -138,11 +138,9 @@ void Scene::draw(QOpenGLShaderProgram* program) if (!m_elytraVisible) { m_cape->draw(program); } else { - glDisable(GL_CULL_FACE); for (auto e : m_elytra) { e->draw(program); } - glEnable(GL_CULL_FACE); } m_capeTexture->release(); } diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp index e4d1eae8d..ca6d6ad27 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -20,11 +20,13 @@ #include #include +#include #include #include #include #include +#include "BuildConfig.h" #include "minecraft/skins/SkinModel.h" #include "rainbow.h" #include "ui/dialogs/skins/draw/BoxGeometry.h" @@ -217,9 +219,6 @@ void SkinOpenGLWindow::paintGL() glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); - // Enable back face culling - glEnable(GL_CULL_FACE); - glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -273,10 +272,10 @@ QColor calculateContrastingColor(const QColor& color) { auto luma = Rainbow::luma(color); if (luma < 0.5) { - constexpr float contrast = 0.05; + constexpr float contrast = 0.05f; return Rainbow::lighten(color, contrast); } else { - constexpr float contrast = 0.2; + constexpr float contrast = 0.2f; return Rainbow::darken(color, contrast); } } @@ -333,6 +332,12 @@ void SkinOpenGLWindow::setElytraVisible(bool visible) bool SkinOpenGLWindow::hasOpenGL() { + if (!QProcessEnvironment::systemEnvironment() + .value(QStringLiteral("%1_DISABLE_GLVULKAN").arg(BuildConfig.LAUNCHER_ENVNAME)) + .isEmpty()) { + return false; + } + QOpenGLContext ctx; return ctx.create(); } diff --git a/launcher/ui/instanceview/InstanceProxyModel.cpp b/launcher/ui/instanceview/InstanceProxyModel.cpp index ab6bef696..f28149a56 100644 --- a/launcher/ui/instanceview/InstanceProxyModel.cpp +++ b/launcher/ui/instanceview/InstanceProxyModel.cpp @@ -62,6 +62,11 @@ bool InstanceProxyModel::subSortLessThan(const QModelIndex& left, const QModelIn QString sortMode = APPLICATION->settings()->get("InstSortMode").toString(); if (sortMode == "LastLaunch") { return pdataLeft->lastLaunch() > pdataRight->lastLaunch(); + } else if (sortMode == "Playtime") { + if (pdataLeft->totalTimePlayed() == pdataRight->totalTimePlayed()) { + return m_naturalSort.compare(pdataLeft->name(), pdataRight->name()) < 0; + } + return pdataLeft->totalTimePlayed() > pdataRight->totalTimePlayed(); } else { return m_naturalSort.compare(pdataLeft->name(), pdataRight->name()) < 0; } diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 8eb82bbae..9a24b7990 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -511,8 +511,7 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) int wpWidth = viewport()->width(); option.rect.setWidth(wpWidth); - for (int i = 0; i < m_groups.size(); ++i) { - VisualGroup* category = m_groups.at(i); + for (auto* category : m_groups) { int y = category->verticalPosition(); y -= verticalOffset(); QRect backup = option.rect; @@ -522,7 +521,6 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) option.rect.setLeft(m_leftMargin); option.rect.setRight(wpWidth - m_rightMargin); category->drawHeader(&painter, option); - y += category->totalHeight() + m_categoryMargin; option.rect = backup; } @@ -644,7 +642,7 @@ void InstanceView::dropEvent(QDropEvent* event) return; } auto instanceId = QString::fromUtf8(mimedata->data("application/x-instanceid")); - auto instanceList = APPLICATION->instances().get(); + auto instanceList = APPLICATION->instances(); instanceList->setInstanceGroup(instanceId, group->text); event->setDropAction(Qt::MoveAction); event->accept(); diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 4f7a61eb5..b68c09171 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -151,7 +151,7 @@ void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& opti QPen pen; pen.setWidth(2); QColor penColor = option.palette.text().color(); - penColor.setAlphaF(0.6); + penColor.setAlphaF(0.6f); pen.setColor(penColor); painter->setPen(pen); painter->setRenderHint(QPainter::Antialiasing); @@ -194,7 +194,7 @@ void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& opti // BEGIN: horizontal line { - penColor.setAlphaF(0.05); + penColor.setAlphaF(0.05f); pen.setColor(penColor); painter->setPen(pen); // startPoint is left + arrow + text + space diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 938befe13..fd8e43398 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -54,7 +54,6 @@ class InstallJavaPage : public QWidget, public BasePage { horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); horizontalLayout->setContentsMargins(0, 0, 0, 0); - majorVersionSelect = new VersionSelectWidget(this); majorVersionSelect->selectCurrent(); majorVersionSelect->setEmptyString(tr("No Java versions are currently available in the meta.")); @@ -122,8 +121,8 @@ class InstallJavaPage : public QWidget, public BasePage { void selectSearch() { javaVersionSelect->selectSearch(); } void loadList() { - majorVersionSelect->loadList(); - javaVersionSelect->loadList(); + majorVersionSelect->loadList(true); + javaVersionSelect->loadList(true); } public slots: @@ -187,13 +186,18 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget : QDialog(parent), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) { auto layout = new QVBoxLayout(this); + // small margins look ugly on macOS on modal windows + #ifndef Q_OS_MACOS layout->setContentsMargins(0, 0, 0, 0); - + #endif container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); + // small margins look ugly on macOS on modal windows + #ifndef Q_OS_MACOS buttonLayout->setContentsMargins(0, 0, 6, 6); + #endif auto refreshLayout = new QHBoxLayout(this); @@ -284,6 +288,11 @@ QList InstallDialog::getPages() new InstallJavaPage("net.adoptium.java", "adoptium", tr("Adoptium")), // Azul new InstallJavaPage("com.azul.java", "azul", tr("Azul Zulu")), + // IBM + /* Must watch out in case the AdoptOpenJDK infrastructure is deprecated. + In case of happening, IBM does not seem to provide as of today (03/2026) an API like Adoptium does and rather uses GitHub directly in its website: `developer.ibm.com`. + GitHub is known for rate limiting requests that do not use an API key from an account. */ + new InstallJavaPage("com.ibm.java", "openj9_hex_custom", tr("IBM Semeru Open")), }; } diff --git a/launcher/ui/java/VersionList.cpp b/launcher/ui/java/VersionList.cpp index f958f064f..7a4e84f33 100644 --- a/launcher/ui/java/VersionList.cpp +++ b/launcher/ui/java/VersionList.cpp @@ -33,9 +33,9 @@ VersionList::VersionList(Meta::Version::Ptr version, QObject* parent) : BaseVers sortVersions(); } -Task::Ptr VersionList::getLoadTask() +Task::Ptr VersionList::getLoadTask(bool forceReload) { - auto task = m_version->loadTask(Net::Mode::Online); + auto task = m_version->loadTask(Net::Mode::Online, forceReload); connect(task.get(), &Task::finished, this, &VersionList::sortVersions); return task; } diff --git a/launcher/ui/java/VersionList.h b/launcher/ui/java/VersionList.h index d334ed564..cf8a7448d 100644 --- a/launcher/ui/java/VersionList.h +++ b/launcher/ui/java/VersionList.h @@ -30,7 +30,7 @@ class VersionList : public BaseVersionList { public: explicit VersionList(Meta::Version::Ptr m_version, QObject* parent = 0); - Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask(bool forceReload = false) override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 7e52b5f65..0cd80521f 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -21,6 +21,7 @@ #include #include "Application.h" +#include "settings/SettingsObject.h" #include "ui/widgets/PageContainer.h" diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index ebe93e5b4..4ed05f98e 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -77,9 +77,12 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); ui->resourceURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->resourceURL)); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); + ui->legacyFMLLibsURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->legacyFMLLibsURL)); ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); + ui->resourceURL->setPlaceholderText(BuildConfig.DEFAULT_RESOURCE_BASE); + ui->legacyFMLLibsURL->setPlaceholderText(BuildConfig.LEGACY_FMLLIBS_BASE_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); loadSettings(); @@ -132,12 +135,19 @@ void APIPage::loadSettings() ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex); + if (bool fallbackMRBlockedMods = s->get("FallbackMRBlockedMods").toBool()) { + ui->FallbackMRBlockedMods->setChecked(fallbackMRBlockedMods); + } + QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); QString metaURL = s->get("MetaURLOverride").toString(); ui->metaURL->setText(metaURL); - QString resourceURL = s->get("ResourceURL").toString(); + ui->metaRefreshOnLaunchCB->setCheckState(s->get("MetaRefreshOnLaunch").toBool() ? Qt::Checked : Qt::Unchecked); + QString resourceURL = s->get("ResourceURLOverride").toString(); ui->resourceURL->setText(resourceURL); + QString fmlLibsURL = s->get("LegacyFMLLibsURLOverride").toString(); + ui->legacyFMLLibsURL->setText(fmlLibsURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); QString modrinthToken = s->get("ModrinthToken").toString(); @@ -158,34 +168,36 @@ void APIPage::applySettings() s->set("MSAClientIDOverride", msaClientID); QUrl metaURL(ui->metaURL->text()); QUrl resourceURL(ui->resourceURL->text()); - // Add required trailing slash - if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) { - QString path = metaURL.path(); - path.append('/'); - metaURL.setPath(path); - } + QUrl fmlLibsURL(ui->legacyFMLLibsURL->text()); - if (!resourceURL.isEmpty() && !resourceURL.path().endsWith('/')) { - QString path = resourceURL.path(); - path.append('/'); - resourceURL.setPath(path); - } + auto addRequiredTrailingSlash = [](QUrl& url) { + if (!url.isEmpty() && !url.path().endsWith('/')) { + QString path = url.path(); + path.append('/'); + url.setPath(path); + } + }; + addRequiredTrailingSlash(metaURL); + addRequiredTrailingSlash(resourceURL); + addRequiredTrailingSlash(fmlLibsURL); auto isLocalhost = [](const QUrl& url) { return url.host() == "localhost" || url.host() == "127.0.0.1" || url.host() == "::1"; }; auto isUnsafe = [isLocalhost](const QUrl& url) { return !url.isEmpty() && url.scheme() == "http" && !isLocalhost(url); }; + auto upgradeToHTTPS = [isUnsafe](QUrl& url) { + if (isUnsafe(url)) { + url.setScheme("https"); + } + }; - // Don't allow HTTP, since meta is basically RCE with all the jar files. - if (isUnsafe(metaURL)) { - metaURL.setScheme("https"); - } - - // Also don't allow HTTP - if (isUnsafe(resourceURL)) { - resourceURL.setScheme("https"); - } + upgradeToHTTPS(metaURL); + upgradeToHTTPS(resourceURL); + upgradeToHTTPS(fmlLibsURL); + s->set("FallbackMRBlockedMods", ui->FallbackMRBlockedMods->checkState()); s->set("MetaURLOverride", metaURL.toString()); - s->set("ResourceURL", resourceURL.toString()); + s->set("MetaRefreshOnLaunch", ui->metaRefreshOnLaunchCB->checkState() == Qt::Checked); + s->set("ResourceURLOverride", resourceURL.toString()); + s->set("LegacyFMLLibsURLOverride", fmlLibsURL.toString()); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); QString modrinthToken = ui->modrinthToken->text(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 834eebb96..25d78a04d 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -32,9 +32,9 @@ 0 - -216 - 816 - 832 + 0 + 825 + 1236 @@ -126,6 +126,13 @@ + + + + Refresh on launch + + + @@ -161,6 +168,34 @@ + + + + Legacy FML Libraries Server + + + + + + You can set this to another server if you have problems with downloading legacy FML libraries (Minecraft 1.5.2 and earlier). + + + Qt::RichText + + + true + + + true + + + + + + + + + @@ -351,6 +386,13 @@ + + + + Enable fallback to Modrinth for blocked mods + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 7e7490d34..06c40f117 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -61,9 +61,8 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new m_accounts = APPLICATION->accounts(); - ui->listView->setModel(m_accounts.get()); + ui->listView->setModel(m_accounts); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); @@ -78,9 +77,9 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new connect(ui->listView, &VersionListView::activated, this, [this](const QModelIndex& index) { m_accounts->setDefaultAccount(m_accounts->at(index.row())); }); - connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); - connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); - connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); + connect(m_accounts, &AccountList::listChanged, this, &AccountListPage::listChanged); + connect(m_accounts, &AccountList::listActivityChanged, this, &AccountListPage::listChanged); + connect(m_accounts, &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); updateButtonStates(); @@ -210,11 +209,17 @@ void AccountListPage::updateButtonStates() bool hasSelection = !selection.empty(); bool accountIsReady = false; bool accountIsOnline = false; + bool accountCanMoveUp = false; + bool accountCanMoveDown = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); accountIsReady = !account->isActive(); accountIsOnline = account->accountType() != AccountType::Offline; + + accountCanMoveUp = selected.row() > 0; + int indexOfLast = m_accounts->count() - 1; + accountCanMoveDown = selected.row() < indexOfLast; } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); @@ -228,6 +233,8 @@ void AccountListPage::updateButtonStates() ui->actionNoDefault->setEnabled(true); ui->actionNoDefault->setChecked(false); } + ui->actionMoveUp->setEnabled(accountCanMoveUp); + ui->actionMoveDown->setEnabled(accountCanMoveDown); ui->listView->resizeColumnToContents(3); } @@ -241,3 +248,21 @@ void AccountListPage::on_actionManageSkins_triggered() dialog.exec(); } } + +void AccountListPage::on_actionMoveUp_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + m_accounts->moveAccount(selected, -1); + } +} + +void AccountListPage::on_actionMoveDown_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + m_accounts->moveAccount(selected, 1); + } +} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 2841b9456..bee56cb58 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -76,6 +76,8 @@ class AccountListPage : public QMainWindow, public BasePage { void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); void on_actionManageSkins_triggered(); + void on_actionMoveUp_triggered(); + void on_actionMoveDown_triggered(); void listChanged(); @@ -88,6 +90,6 @@ class AccountListPage : public QMainWindow, public BasePage { private: void changeEvent(QEvent* event) override; QMenu* createPopupMenu() override; - shared_qobject_ptr m_accounts; + AccountList* m_accounts; Ui::AccountListPage* ui; }; diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index c9b770ab2..6fa004ed7 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -58,6 +58,8 @@ + + @@ -105,6 +107,16 @@ Remo&ve + + + Move &Up + + + + + Move &Down + + diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 6a44c9290..d780ad542 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -55,7 +55,6 @@ #include "java/JavaUtils.h" #include -#include #include "Application.h" #include "settings/SettingsObject.h" diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index f4be75782..94c582775 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -38,6 +38,7 @@ #include #include "Application.h" +#include "settings/SettingsObject.h" #include "ui/widgets/LanguageSelectionWidget.h" LanguagePage::LanguagePage(QWidget* parent) : QWidget(parent) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 7a0f11c83..1e1fed8f8 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -62,7 +62,9 @@ enum InstSortMode { // Sort alphabetically by name. Sort_Name, // Sort by which instance was launched most recently. - Sort_LastLaunch + Sort_LastLaunch, + // Sort by which instance has the most playtime. + Sort_Playtime, }; LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) @@ -71,6 +73,7 @@ LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::Launch ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); + ui->sortingModeGroup->setId(ui->sortByPlaytimeBtn, Sort_Playtime); loadSettings(); @@ -90,12 +93,12 @@ bool LauncherPage::apply() void LauncherPage::on_instDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - if (FS::checkProblemticPathJava(QDir(cooked_dir))) { + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + if (FS::checkProblemticPathJava(QDir(cookedDir))) { QMessageBox warning; warning.setText( tr("You're trying to specify an instance folder which\'s path " @@ -108,9 +111,9 @@ void LauncherPage::on_instDirBrowseBtn_clicked() warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); if (result == QMessageBox::Ok) { - ui->instDirTextBox->setText(cooked_dir); + ui->instDirTextBox->setText(cookedDir); } - } else if (DesktopServices::isFlatpak() && raw_dir.startsWith("/run/user")) { + } else if (DesktopServices::isFlatpak() && rawDir.startsWith("/run/user")) { QMessageBox warning; warning.setText(tr("You're trying to specify an instance folder " "which was granted temporarily via Flatpak.\n" @@ -123,64 +126,64 @@ void LauncherPage::on_instDirBrowseBtn_clicked() warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); if (result == QMessageBox::Ok) { - ui->instDirTextBox->setText(cooked_dir); + ui->instDirTextBox->setText(cookedDir); } } else { - ui->instDirTextBox->setText(cooked_dir); + ui->instDirTextBox->setText(cookedDir); } } } void LauncherPage::on_iconsDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - ui->iconsDirTextBox->setText(cooked_dir); + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + ui->iconsDirTextBox->setText(cookedDir); } } void LauncherPage::on_modsDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - ui->modsDirTextBox->setText(cooked_dir); + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + ui->modsDirTextBox->setText(cookedDir); } } void LauncherPage::on_downloadsDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Downloads Folder"), ui->downloadsDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Downloads Folder"), ui->downloadsDirTextBox->text()); - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - ui->downloadsDirTextBox->setText(cooked_dir); + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + ui->downloadsDirTextBox->setText(cookedDir); } } void LauncherPage::on_javaDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Java Folder"), ui->javaDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Java Folder"), ui->javaDirTextBox->text()); - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - ui->javaDirTextBox->setText(cooked_dir); + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + ui->javaDirTextBox->setText(cookedDir); } } void LauncherPage::on_skinsDirBrowseBtn_clicked() { - QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); + QString rawDir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { - QString cooked_dir = FS::NormalizePath(raw_dir); - ui->skinsDirTextBox->setText(cooked_dir); + if (!rawDir.isEmpty() && QDir(rawDir).exists()) { + QString cookedDir = FS::NormalizePath(rawDir); + ui->skinsDirTextBox->setText(cookedDir); } } @@ -191,7 +194,7 @@ void LauncherPage::on_metadataEnableBtn_clicked() void LauncherPage::applySettings() { - auto s = APPLICATION->settings(); + auto* s = APPLICATION->settings(); // Updates if (APPLICATION->updater()) { @@ -227,6 +230,9 @@ void LauncherPage::applySettings() case Sort_LastLaunch: s->set("InstSortMode", "LastLaunch"); break; + case Sort_Playtime: + s->set("InstSortMode", "Playtime"); + break; case Sort_Name: default: s->set("InstSortMode", "Name"); @@ -244,11 +250,13 @@ void LauncherPage::applySettings() // Mods s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); + s->set("ShowModIncompat", ui->showModIncompatCheckBox->isChecked()); s->set("SkipModpackUpdatePrompt", !ui->modpackUpdatePromptBtn->isChecked()); + s->set("DownloadGameFilesDuringInstanceCreation", ui->downloadGameFilesBtn->isChecked()); } void LauncherPage::loadSettings() { - auto s = APPLICATION->settings(); + auto* s = APPLICATION->settings(); // Updates if (APPLICATION->updater()) { ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates()); @@ -280,6 +288,8 @@ void LauncherPage::loadSettings() QString sortMode = s->get("InstSortMode").toString(); if (sortMode == "LastLaunch") { ui->sortLastLaunchedBtn->setChecked(true); + } else if (sortMode == "Playtime"){ + ui->sortByPlaytimeBtn->setChecked(true); } else { ui->sortByNameBtn->setChecked(true); } @@ -293,7 +303,9 @@ void LauncherPage::loadSettings() ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); ui->dependenciesEnableBtn->setChecked(!s->get("ModDependenciesDisabled").toBool()); + ui->showModIncompatCheckBox->setChecked(s->get("ShowModIncompat").toBool()); ui->modpackUpdatePromptBtn->setChecked(!s->get("SkipModpackUpdatePrompt").toBool()); + ui->downloadGameFilesBtn->setChecked(s->get("DownloadGameFilesDuringInstanceCreation").toBool()); } void LauncherPage::retranslate() diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 0debe3f4d..c98cb1032 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,9 +41,9 @@ 0 - 0 + -149 746 - 1194 + 1222 @@ -83,6 +83,16 @@ + + + + By total time &played + + + sortingModeGroup + + + @@ -251,8 +261,11 @@ + + Folder where Prism Launcher stores automatically downloaded Java versions. Do NOT set this to your system Java installation. + - &Java: + &Auto Java Download: javaDirTextBox @@ -418,6 +431,16 @@ + + + + Currently this just shows mods which are not marked as compatible with the current Minecraft version. + + + Detect and show mod incompatibilities (experimental) + + + @@ -431,6 +454,25 @@ + + + + Instance Creation + + + + + + Downloads required game files while creating the instance. Disable this to skip the initial download and fetch files when the instance is launched instead. + + + Download game files during instance creation + + + + + + @@ -641,6 +683,12 @@ scrollArea + sortByNameBtn + sortLastLaunchedBtn + sortByPlaytimeBtn + askToRenameDirBtn + alwaysRenameDirBtn + neverRenameDirBtn preferMenuBarCheckBox autoUpdateCheckBox updateIntervalSpinBox @@ -660,7 +708,9 @@ downloadsDirMoveCheckBox metadataEnableBtn dependenciesEnableBtn + showModIncompatCheckBox modpackUpdatePromptBtn + downloadGameFilesBtn lineLimitSpinBox checkStopLogging numberOfConcurrentTasksSpinBox @@ -671,7 +721,7 @@ - + diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index a56cc9b79..eb59fbb1e 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -25,7 +25,7 @@ #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" -DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) +DataPackPage::DataPackPage(BaseInstance* instance, DataPackFolderModel* model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); @@ -39,9 +39,9 @@ DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptractionUpdateItem, &QAction::triggered, this, &DataPackPage::updateDataPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - auto updateMenu = new QMenu(this); + auto* updateMenu = new QMenu(this); - auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + auto* update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &DataPackPage::updateDataPacks); updateMenu->addAction(ui->actionResetItemMetadata); @@ -64,10 +64,9 @@ void DataPackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] cons void DataPackPage::downloadDataPacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance - - auto profile = static_cast(m_instance)->getPackProfile(); + } m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); @@ -78,9 +77,9 @@ void DataPackPage::downloadDataPacks() void DataPackPage::downloadDialogFinished(int result) { - if (result) { - auto tasks = new ConcurrentTask(tr("Download Data Packs"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (result != 0) { + auto* tasks = new ConcurrentTask(tr("Download Data Packs"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -90,8 +89,9 @@ void DataPackPage::downloadDialogFinished(int result) }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -110,16 +110,17 @@ void DataPackPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) + if (m_downloadDialog) { m_downloadDialog->deleteLater(); + } } void DataPackPage::updateDataPacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); return; @@ -133,27 +134,29 @@ void DataPackPage::updateDataPacks() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedResources(selection); - bool use_all = mods_list.empty(); - if (use_all) - mods_list = m_model->allResources(); + auto modsList = m_model->selectedResources(selection); + bool useAll = modsList.empty(); + if (useAll) { + modsList = m_model->allResources(); + } - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, { ModPlatform::ModLoaderType::DataPack }); - update_dialog.checkCandidates(); + ResourceUpdateDialog updateDialog(this, m_instance, m_model, modsList, false, { ModPlatform::ModLoaderType::DataPack }); + updateDialog.checkCandidates(); - if (update_dialog.aborted()) { + if (updateDialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The data pack updater was aborted!"), QMessageBox::Warning)->show(); return; } - if (update_dialog.noUpdates()) { - QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; - if (mods_list.size() > 1) { - if (use_all) { + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (useAll) { message = tr("All data packs are up-to-date! :)"); } else { message = tr("All selected data packs are up-to-date! :)"); @@ -163,9 +166,9 @@ void DataPackPage::updateDataPacks() return; } - if (update_dialog.exec()) { - auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -181,7 +184,7 @@ void DataPackPage::updateDataPacks() tasks->deleteLater(); }); - for (auto task : update_dialog.getTasks()) { + for (const auto& task : updateDialog.getTasks()) { tasks->addTask(task); } @@ -197,8 +200,9 @@ void DataPackPage::deleteDataPackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedDataPacks(selection).length(); - if (selectionCount == 0) + if (selectionCount == 0) { return; + } if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 data packs.\n" @@ -207,8 +211,9 @@ void DataPackPage::deleteDataPackMetadata() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } m_model->deleteMetadata(selection); @@ -216,8 +221,9 @@ void DataPackPage::deleteDataPackMetadata() void DataPackPage::changeDataPackVersion() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); @@ -226,19 +232,21 @@ void DataPackPage::changeDataPackVersion() const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); - if (rows.count() != 1) + if (rows.count() != 1) { return; + } Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); - if (resource.metadata() == nullptr) + if (resource.metadata() == nullptr) { return; + } - ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance); + ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance, true); mdownload.setResourceMetadata(resource.metadata()); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (mdownload.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -248,8 +256,9 @@ void DataPackPage::changeDataPackVersion() }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -268,14 +277,15 @@ void DataPackPage::changeDataPackVersion() GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) { - auto layout = new QVBoxLayout(this); + auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); connect(instance->settings()->getSetting("GlobalDataPacksEnabled").get(), &Setting::SettingChanged, this, [this] { updateContent(); - if (m_container != nullptr) + if (m_container != nullptr) { m_container->refreshContainer(); + } }); connect(instance->settings()->getSetting("GlobalDataPacksPath").get(), &Setting::SettingChanged, this, @@ -284,24 +294,27 @@ GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* par QString GlobalDataPackPage::displayName() const { - if (m_underlyingPage == nullptr) + if (m_underlyingPage == nullptr) { return {}; + } return m_underlyingPage->displayName(); } QIcon GlobalDataPackPage::icon() const { - if (m_underlyingPage == nullptr) + if (m_underlyingPage == nullptr) { return {}; + } return m_underlyingPage->icon(); } QString GlobalDataPackPage::helpPage() const { - if (m_underlyingPage == nullptr) + if (m_underlyingPage == nullptr) { return {}; + } return m_underlyingPage->helpPage(); } @@ -318,21 +331,24 @@ bool GlobalDataPackPage::apply() void GlobalDataPackPage::openedImpl() { - if (m_underlyingPage != nullptr) + if (m_underlyingPage != nullptr) { m_underlyingPage->openedImpl(); + } } void GlobalDataPackPage::closedImpl() { - if (m_underlyingPage != nullptr) + if (m_underlyingPage != nullptr) { m_underlyingPage->closedImpl(); + } } void GlobalDataPackPage::updateContent() { if (m_underlyingPage != nullptr) { - if (m_container->selectedPage() == this) + if (m_container->selectedPage() == this) { m_underlyingPage->closedImpl(); + } m_underlyingPage->apply(); @@ -347,8 +363,9 @@ void GlobalDataPackPage::updateContent() m_underlyingPage->setParentContainer(m_container); m_underlyingPage->updateExtraInfo = [this](QString id, QString value) { updateExtraInfo(std::move(id), std::move(value)); }; - if (m_container->selectedPage() == this) + if (m_container->selectedPage() == this) { m_underlyingPage->openedImpl(); + } layout()->addWidget(m_underlyingPage); } diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index 4ca81bdd1..a3e6627d4 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -26,7 +26,7 @@ class DataPackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + explicit DataPackPage(BaseInstance* instance, DataPackFolderModel* model, QWidget* parent = nullptr); QString displayName() const override { return QObject::tr("Data Packs"); } QIcon icon() const override { return QIcon::fromTheme("datapacks"); } @@ -43,7 +43,7 @@ class DataPackPage : public ExternalResourcesPage { void changeDataPackVersion(); private: - std::shared_ptr m_model; + DataPackFolderModel* m_model; QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 9174d6ee4..da7fa3ee0 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -47,7 +47,7 @@ #include #include -ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) +ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, ResourceFolderModel* model, QWidget* parent) : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) { ui->setupUi(this); @@ -58,7 +58,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_filterModel->setDynamicSortFilter(true); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); - m_filterModel->setSourceModel(m_model.get()); + m_filterModel->setSourceModel(m_model); m_filterModel->setFilterKeyColumn(-1); ui->treeView->setModel(m_filterModel); // must come after setModel @@ -98,12 +98,12 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared }; connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); - connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); - connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra); + connect(model, &ResourceFolderModel::updateFinished, this, updateExtra); + connect(model, &ResourceFolderModel::parseFinished, this, updateExtra); connect(selection_model, &QItemSelectionModel::selectionChanged, this, [this] { updateActions(); }); - connect(m_model.get(), &ResourceFolderModel::rowsInserted, this, [this] { updateActions(); }); - connect(m_model.get(), &ResourceFolderModel::rowsRemoved, this, [this] { updateActions(); }); + connect(m_model, &ResourceFolderModel::rowsInserted, this, [this] { updateActions(); }); + connect(m_model, &ResourceFolderModel::rowsRemoved, this, [this] { updateActions(); }); auto viewHeader = ui->treeView->header(); viewHeader->setContextMenuPolicy(Qt::CustomContextMenu); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 00bb5d17d..7f4320648 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -20,7 +20,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + explicit ExternalResourcesPage(BaseInstance* instance, ResourceFolderModel* model, QWidget* parent = nullptr); virtual ~ExternalResourcesPage(); virtual QString displayName() const override = 0; @@ -68,7 +68,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { BaseInstance* m_instance = nullptr; Ui::ExternalResourcesPage* ui = nullptr; - std::shared_ptr m_model; + ResourceFolderModel* m_model; QSortFilterProxyModel* m_filterModel = nullptr; QString m_fileSelectionFilter; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index aca47e2c7..79d5944eb 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -44,8 +44,7 @@ class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) - : MinecraftSettingsWidget(std::move(instance), parent) + explicit InstanceSettingsPage(MinecraftInstance* instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(instance, parent) { connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceSettingsPage::loadSettings); diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 9f978ecb7..770698961 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -128,7 +128,7 @@ QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& v return QModelIndex(); } -LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) +LogPage::LogPage(BaseInstance* instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); @@ -153,7 +153,7 @@ LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(ne if (launchTask) { setInstanceLaunchTaskChanged(launchTask, true); } - connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged); + connect(m_instance, &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged); } auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); @@ -203,7 +203,7 @@ void LogPage::UIToModelState() m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } -void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial) +void LogPage::setInstanceLaunchTaskChanged(LaunchTask* proc, bool initial) { m_process = proc; if (m_process) { @@ -220,7 +220,7 @@ void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr proc, } } -void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr proc) +void LogPage::onInstanceLaunchTaskChanged(LaunchTask* proc) { setInstanceLaunchTaskChanged(proc, false); } diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 636a8b70d..ef93f2cc0 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -63,7 +63,7 @@ class LogPage : public QWidget, public BasePage { Q_OBJECT public: - explicit LogPage(InstancePtr instance, QWidget* parent = 0); + explicit LogPage(BaseInstance* instance, QWidget* parent = 0); virtual ~LogPage(); virtual QString displayName() const override { return tr("Minecraft Log"); } virtual QIcon icon() const override { return QIcon::fromTheme("log"); } @@ -88,17 +88,17 @@ class LogPage : public QWidget, public BasePage { void findNextActivated(); void findPreviousActivated(); - void onInstanceLaunchTaskChanged(shared_qobject_ptr proc); + void onInstanceLaunchTaskChanged(LaunchTask* proc); private: void modelStateToUI(); void UIToModelState(); - void setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial); + void setInstanceLaunchTaskChanged(LaunchTask* proc, bool initial); private: Ui::LogPage* ui; - InstancePtr m_instance; - shared_qobject_ptr m_process; + BaseInstance* m_instance; + LaunchTask* m_process; LogFormatProxyModel* m_proxy; shared_qobject_ptr m_model; diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 230cf1bdf..d2683fa92 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -4,6 +4,7 @@ #include "ManagedPackPage.h" #include +#include #include #include #include "modplatform/ModIndex.h" @@ -102,6 +103,9 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi ui->versionsComboBox->setStyle(comboStyle); } + ui->versionsComboBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionsComboBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + ui->reloadButton->setVisible(false); connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) { ui->reloadButton->setVisible(false); @@ -123,6 +127,8 @@ ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_wi } QDesktopServices::openUrl(url); }); + + connect(ui->urlLine, &QLineEdit::textChanged, this, [this](QString text) { m_inst->settings()->set("ManagedPackURL", text.trimmed()); }); } ManagedPackPage::~ManagedPackPage() @@ -138,16 +144,20 @@ void ManagedPackPage::openedImpl() ui->packOrigin->hide(); ui->packOriginLabel->hide(); ui->versionsComboBox->hide(); - ui->updateButton->hide(); - ui->updateToVersionLabel->hide(); - ui->updateFromFileButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + ui->updateToVersionLabel->setText(tr("URL:")); + ui->updateButton->setText(tr("Update Pack")); + ui->updateButton->setDisabled(false); + ui->urlLine->setText(m_inst->settings()->get("ManagedPackURL").toString().trimmed()); ui->packName->setText(m_inst->name()); ui->changelogTextBrowser->setText(tr("This is a local modpack.\n" - "This can be updated only using a file in %1 format\n") + "This can be updated either using a file in %1 format or an URL.\n" + "Do not use a different format than the one mentioned as it may break the instance.\n" + "Make sure you also trust the URL.\n") .arg(displayName())); return; } + ui->urlLine->hide(); ui->packName->setText(m_inst->getManagedPackName()); ui->packVersion->setText(m_inst->getManagedPackVersionName()); ui->packOrigin->setText(tr("Website: %2 | Pack ID: %3 | Version ID: %4") @@ -192,23 +202,24 @@ bool ManagedPackPage::runUpdateTask(InstanceTask* task) unique_qobject_ptr wrapped_task(APPLICATION->instances()->wrapInstanceTask(task)); - connect(task, &Task::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); - connect(task, &Task::succeeded, [this, task]() { + connect(wrapped_task.get(), &Task::failed, + [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(wrapped_task.get(), &Task::succeeded, [this, task]() { QStringList warnings = task->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } }); - connect(task, &Task::aborted, [this] { + connect(wrapped_task.get(), &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(task); + loadDialog.execWithTask(wrapped_task.get()); - return task->wasSuccessful(); + return wrapped_task->wasSuccessful(); } void ManagedPackPage::suggestVersion() @@ -250,14 +261,16 @@ void ModrinthManagedPackPage::parseManagedPack() qDebug() << "Parsing Modrinth pack"; // No need for the extra work because we already have everything we need. - if (m_loaded) + if (m_loaded) { return; + } - if (m_fetch_job && m_fetch_job->isRunning()) + if (m_fetch_job && m_fetch_job->isRunning()) { m_fetch_job->abort(); + } ResourceAPI::Callback> callbacks{}; - m_pack = { m_inst->getManagedPackID() }; + m_pack = { .addonId = m_inst->getManagedPackID() }; // Use default if no callbacks are set callbacks.on_succeed = [this](auto& doc) { @@ -274,8 +287,9 @@ void ModrinthManagedPackPage::parseManagedPack() // NOTE: the id from version isn't the same id in the modpack format spec... // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. - if (version.version == m_inst->getManagedPackVersionName()) + if (version.version == m_inst->getManagedPackVersionName()) { name = tr("%1 (Current)").arg(name); + } ui->versionsComboBox->addItem(name, version.fileId); } @@ -284,10 +298,14 @@ void ModrinthManagedPackPage::parseManagedPack() m_loaded = true; }; - callbacks.on_fail = [this](QString reason, int) { setFailState(); }; + callbacks.on_fail = [this](const QString& /*reason*/, int) { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); }; - m_fetch_job = m_api.getProjectVersions( - { std::make_shared(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + m_fetch_job = m_api.getProjectVersions({ .pack = std::make_shared(m_pack), + .mcVersions = {}, + .loaders = {}, + .resourceType = ModPlatform::ResourceType::Modpack, + .includeChangelog = true }, + std::move(callbacks)); ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); @@ -339,6 +357,11 @@ void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const void ModrinthManagedPackPage::update() { + auto customURL = m_inst->settings()->get("ManagedPackURL").toString().trimmed(); + if (m_inst->getManagedPackID().isEmpty() && !customURL.isEmpty()) { + updatePack(customURL); + return; + } auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); @@ -346,25 +369,7 @@ void ModrinthManagedPackPage::update() } auto version = m_pack.versions.at(index); - QMap extra_info; - // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. - extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", version.fileId.toString()); - extra_info.insert("original_instance_id", m_inst->id()); - - auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); - - InstanceName inst_name(m_inst->getManagedPackName(), version.version); - inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); - extracted->setName(inst_name); - - extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); - extracted->setIcon(m_inst->iconKey()); - extracted->setConfirmUpdate(false); - - // Run our task then handle the result - auto did_succeed = runUpdateTask(extracted); - onUpdateTaskCompleted(did_succeed); + updatePack(version.downloadUrl, version.fileId.toString(), version.version); } void ModrinthManagedPackPage::updateFromFile() @@ -372,21 +377,8 @@ void ModrinthManagedPackPage::updateFromFile() auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("Modrinth pack") + " (*.mrpack *.zip)"); if (output.isEmpty()) return; - QMap extra_info; - extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", QString()); - extra_info.insert("original_instance_id", m_inst->id()); - auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); - - extracted->setName(m_inst->name()); - extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); - extracted->setIcon(m_inst->iconKey()); - extracted->setConfirmUpdate(false); - - // Run our task then handle the result - auto did_succeed = runUpdateTask(extracted); - onUpdateTaskCompleted(did_succeed); + updatePack(output); } // FLAME @@ -422,14 +414,16 @@ void FlameManagedPackPage::parseManagedPack() } // No need for the extra work because we already have everything we need. - if (m_loaded) + if (m_loaded) { return; + } - if (m_fetch_job && m_fetch_job->isRunning()) + if (m_fetch_job && m_fetch_job->isRunning()) { m_fetch_job->abort(); + } QString id = m_inst->getManagedPackID(); - m_pack = { id }; + m_pack = { .addonId = id }; ResourceAPI::Callback> callbacks{}; @@ -446,8 +440,9 @@ void FlameManagedPackPage::parseManagedPack() for (const auto& version : m_pack.versions) { QString name = version.getVersionDisplayString(); - if (version.fileId == m_inst->getManagedPackVersionID().toInt()) + if (version.fileId == m_inst->getManagedPackVersionID().toInt()) { name = tr("%1 (Current)").arg(name); + } ui->versionsComboBox->addItem(name, QVariant(version.fileId)); } @@ -456,10 +451,14 @@ void FlameManagedPackPage::parseManagedPack() m_loaded = true; }; - callbacks.on_fail = [this](QString reason, int) { setFailState(); }; + callbacks.on_fail = [this](const QString& /*reason*/, int) { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); }; - m_fetch_job = m_api.getProjectVersions( - { std::make_shared(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + m_fetch_job = m_api.getProjectVersions({ .pack = std::make_shared(m_pack), + .mcVersions = {}, + .loaders = {}, + .resourceType = ModPlatform::ResourceType::Modpack, + .includeChangelog = true }, + std::move(callbacks)); m_fetch_job->start(); } @@ -487,6 +486,11 @@ void FlameManagedPackPage::suggestVersion() void FlameManagedPackPage::update() { + auto customURL = m_inst->settings()->get("ManagedPackURL").toString().trimmed(); + if (m_inst->getManagedPackID().isEmpty() && !customURL.isEmpty()) { + updatePack(customURL); + return; + } auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); @@ -494,20 +498,7 @@ void FlameManagedPackPage::update() } auto version = m_pack.versions.at(index); - QMap extra_info; - extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", version.fileId.toString()); - extra_info.insert("original_instance_id", m_inst->id()); - - auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); - - extracted->setName(m_inst->name()); - extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); - extracted->setIcon(m_inst->iconKey()); - extracted->setConfirmUpdate(false); - - auto did_succeed = runUpdateTask(extracted); - onUpdateTaskCompleted(did_succeed); + updatePack(version.downloadUrl, version.fileId.toString()); } void FlameManagedPackPage::updateFromFile() @@ -516,19 +507,33 @@ void FlameManagedPackPage::updateFromFile() if (output.isEmpty()) return; + updatePack(output); +} + +void ManagedPackPage::updatePack(const QUrl& url, QString versionID, QString versionName) +{ QMap extra_info; + // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", QString()); + extra_info.insert("pack_version_id", versionID); extra_info.insert("original_instance_id", m_inst->id()); - auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); + auto extracted = new InstanceImportTask(url, this, std::move(extra_info)); - extracted->setName(m_inst->name()); + if (versionName.isEmpty()) { + extracted->setName(m_inst->name()); + } else { + InstanceName inst_name(m_inst->getManagedPackName(), versionName); + inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), versionName)); + extracted->setName(inst_name); + } extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); + // Run our task then handle the result auto did_succeed = runUpdateTask(extracted); onUpdateTaskCompleted(did_succeed); } + #include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index f319ed069..4b7332896 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -86,6 +86,8 @@ class ManagedPackPage : public QWidget, public BasePage { */ bool runUpdateTask(InstanceTask*); + void updatePack(const QUrl& url, QString versionID = {}, QString versionName = {}); + protected: InstanceWindow* m_instance_window = nullptr; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 62641bc82..5ed80400a 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -137,6 +137,9 @@ + + + diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp index 915df55d4..0a719431d 100644 --- a/launcher/ui/pages/instance/McClient.cpp +++ b/launcher/ui/pages/instance/McClient.cpp @@ -1,18 +1,17 @@ +#include "McClient.h" + #include #include #include #include +#include -#include +#include "Exception.h" #include "Json.h" -#include "McClient.h" -// 7 first bits -#define SEGMENT_BITS 0x7F -// last bit -#define CONTINUE_BIT 0x80 - -McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} +McClient::McClient(QObject* parent, QString domain, QString ip, const uint16_t port) + : QObject(parent), m_domain(std::move(domain)), m_ip(std::move(ip)), m_port(port) +{} void McClient::getStatusData() { @@ -33,13 +32,12 @@ void McClient::getStatusData() void McClient::sendRequest() { QByteArray data; - writeVarInt(data, 0x00); // packet ID - writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) - writeVarInt(data, m_domain.size()); // server address length - writeString(data, m_domain.toStdString()); // server address - writeFixedInt(data, m_port, 2); // server port - writeVarInt(data, 0x01); // next state - writePacketToSocket(data); // send handshake packet + writeVarInt(data, 0x00); // packet ID + writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) + writeString(data, m_domain); // server address + writeUInt16(data, m_port); // server port + writeVarInt(data, 0x01); // next state + writePacketToSocket(data); // send handshake packet writeVarInt(data, 0x00); // packet ID writePacketToSocket(data); // send status packet @@ -47,23 +45,27 @@ void McClient::sendRequest() void McClient::readRawResponse() { - if (m_responseReadState == 2) { + if (m_responseReadState == ResponseReadState::Finished) { return; } m_resp.append(m_socket.readAll()); - if (m_responseReadState == 0 && m_resp.size() >= 5) { + if (m_responseReadState == ResponseReadState::Waiting && m_resp.size() >= 5) { m_wantedRespLength = readVarInt(m_resp); - m_responseReadState = 1; + m_responseReadState = ResponseReadState::GotLength; } - if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { + if (m_responseReadState == ResponseReadState::GotLength && m_resp.size() >= m_wantedRespLength) { if (m_resp.size() > m_wantedRespLength) { qDebug().nospace() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " - << m_resp.size() << " received)"; + << m_resp.size() << " received)"; } - parseResponse(); - m_responseReadState = 2; + try { + parseResponse(); + } catch (const Exception& e) { + emitFail(e.cause()); + } + m_responseReadState = ResponseReadState::Finished; } } @@ -71,7 +73,7 @@ void McClient::parseResponse() { qDebug() << "Received response successfully"; - int packetID = readVarInt(m_resp); + const int packetID = readVarInt(m_resp); if (packetID != 0x00) { throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16)); } @@ -80,7 +82,7 @@ void McClient::parseResponse() // 'resp' should now be the JSON string QJsonParseError parseError; - QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError); + const QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError); if (parseError.error != QJsonParseError::NoError) { qDebug() << "Failed to parse JSON:" << parseError.errorString(); emitFail(parseError.errorString()); @@ -89,18 +91,23 @@ void McClient::parseResponse() emitSucceed(doc.object()); } +// NOLINTBEGIN(*-signed-bitwise) + // From https://wiki.vg/Protocol#VarInt_and_VarLong +constexpr uint8_t g_varIntValueMask = 0x7F; +constexpr uint8_t g_varIntContinue = 0x80; + void McClient::writeVarInt(QByteArray& data, int value) { - while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits + while ((value & ~g_varIntValueMask) != 0) { // check if the value is too big to fit in 7 bits // Write 7 bits - data.append((value & SEGMENT_BITS) | CONTINUE_BIT); + data.append(static_cast((value & ~g_varIntValueMask) | g_varIntContinue)); // NOLINT(*-narrowing-conversions) // Erase theses 7 bits from the value to write // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone value >>= 7; } - data.append(value); + data.append(static_cast(value)); // NOLINT(*-narrowing-conversions) } // From https://wiki.vg/Protocol#VarInt_and_VarLong @@ -108,53 +115,56 @@ int McClient::readVarInt(QByteArray& data) { int value = 0; int position = 0; - char currentByte; while (position < 32) { - currentByte = readByte(data); - value |= (currentByte & SEGMENT_BITS) << position; + const uint8_t currentByte = readByte(data); + value |= (currentByte & g_varIntValueMask) << position; - if ((currentByte & CONTINUE_BIT) == 0) + if ((currentByte & g_varIntContinue) == 0) { break; + } position += 7; } - if (position >= 32) + if (position >= 32) { throw Exception("VarInt is too big"); + } return value; } -char McClient::readByte(QByteArray& data) +// NOLINTEND(*-signed-bitwise) + +uint8_t McClient::readByte(QByteArray& data) { if (data.isEmpty()) { throw Exception("No more bytes to read"); } - char byte = data.at(0); + const uint8_t byte = data.at(0); data.remove(0, 1); return byte; } -// write number with specified size in big endian format -void McClient::writeFixedInt(QByteArray& data, int value, int size) +void McClient::writeUInt16(QByteArray& data, const uint16_t value) { - for (int i = size - 1; i >= 0; i--) { - data.append((value >> (i * 8)) & 0xFF); - } + QDataStream stream(&data, QIODeviceBase::Append); + stream.setByteOrder(QDataStream::BigEndian); + stream << value; } -void McClient::writeString(QByteArray& data, const std::string& value) +void McClient::writeString(QByteArray& data, const QString& value) { - data.append(value.c_str()); + writeVarInt(data, static_cast(value.size())); + data.append(value.toUtf8()); } void McClient::writePacketToSocket(QByteArray& data) { // we prefix the packet with its length QByteArray dataWithSize; - writeVarInt(dataWithSize, data.size()); + writeVarInt(dataWithSize, static_cast(data.size())); dataWithSize.append(data); // write it to the socket @@ -164,7 +174,7 @@ void McClient::writePacketToSocket(QByteArray& data) data.clear(); } -void McClient::emitFail(QString error) +void McClient::emitFail(const QString& error) { qDebug() << "Minecraft server ping for status error:" << error; emit failed(error); @@ -173,6 +183,6 @@ void McClient::emitFail(QString error) void McClient::emitSucceed(QJsonObject data) { - emit succeeded(data); + emit succeeded(std::move(data)); emit finished(); } diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h index 633e7aaed..c1cb3d748 100644 --- a/launcher/ui/pages/instance/McClient.h +++ b/launcher/ui/pages/instance/McClient.h @@ -1,53 +1,54 @@ #pragma once + #include -#include #include #include #include -#include - // Client for the Minecraft protocol class McClient : public QObject { Q_OBJECT - QString m_domain; - QString m_ip; - short m_port; - QTcpSocket m_socket; - - // 0: did not start reading the response yet - // 1: read the response length, still reading the response - // 2: finished reading the response - unsigned m_responseReadState = 0; - unsigned m_wantedRespLength = 0; - QByteArray m_resp; - public: - explicit McClient(QObject* parent, QString domain, QString ip, short port); + explicit McClient(QObject* parent, QString domain, QString ip, uint16_t port); //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data void getStatusData(); + signals: + void succeeded(QJsonObject data); + void failed(QString error); + void finished(); + + private: + static uint8_t readByte(QByteArray& data); + static int readVarInt(QByteArray& data); + static void writeUInt16(QByteArray& data, uint16_t value); + static void writeString(QByteArray& data, const QString& value); + static void writeVarInt(QByteArray& data, int value); + private: void sendRequest(); //! Accumulate data until we have a full response, then call parseResponse() once void readRawResponse(); void parseResponse(); - - void writeVarInt(QByteArray& data, int value); - int readVarInt(QByteArray& data); - char readByte(QByteArray& data); - //! write number with specified size in big endian format - void writeFixedInt(QByteArray& data, int value, int size); - void writeString(QByteArray& data, const std::string& value); - void writePacketToSocket(QByteArray& data); - void emitFail(QString error); + void emitFail(const QString& error); void emitSucceed(QJsonObject data); - signals: - void succeeded(QJsonObject data); - void failed(QString error); - void finished(); + private: + enum class ResponseReadState : uint8_t { + Waiting, + GotLength, + Finished + }; + + QString m_domain; + QString m_ip; + uint16_t m_port; + QTcpSocket m_socket; + + ResponseReadState m_responseReadState = ResponseReadState::Waiting; + int32_t m_wantedRespLength = 0; + QByteArray m_resp; }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8ac091018..99c78647c 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -37,6 +37,7 @@ */ #include "ModFolderPage.h" +#include "minecraft/mod/Resource.h" #include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_ExternalResourcesPage.h" @@ -66,7 +67,7 @@ #include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" -ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent) +ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent) : ExternalResourcesPage(inst, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Mods")); @@ -80,9 +81,9 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - auto updateMenu = new QMenu(this); + auto* updateMenu = new QMenu(this); - auto update = updateMenu->addAction(tr("Check for Updates")); + auto* update = updateMenu->addAction(tr("Check for Updates")); connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); updateMenu->addAction(ui->actionVerifyItemDependencies); @@ -91,7 +92,7 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool()); connect(depsDisabled.get(), &Setting::SettingChanged, this, - [this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); + [this](const Setting&, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); @@ -133,18 +134,39 @@ void ModFolderPage::removeItems(const QItemSelection& selection) QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } - m_model->deleteResources(selection.indexes()); + + auto indexes = selection.indexes(); + auto affected = m_model->getAffectedMods(indexes, EnableAction::DISABLE); + if (!affected.isEmpty()) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Disable"), + tr("The mods you are trying to delete are required by %1 mods.\n" + "Do you want to disable them?") + .arg(affected.length()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, + QMessageBox::Cancel) + ->exec(); + + if (response == QMessageBox::Cancel) { + return; + } + if (response == QMessageBox::Yes) { + m_model->setResourceEnabled(affected, EnableAction::DISABLE); + } + } + m_model->deleteResources(indexes); } void ModFolderPage::downloadMods() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); + auto* profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; @@ -160,9 +182,9 @@ void ModFolderPage::downloadMods() void ModFolderPage::downloadDialogFinished(int result) { - if (result) { - auto tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (result != 0) { + auto* tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -172,8 +194,9 @@ void ModFolderPage::downloadDialogFinished(int result) }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -192,16 +215,18 @@ void ModFolderPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) + if (m_downloadDialog) { m_downloadDialog->deleteLater(); + } } void ModFolderPage::updateMods(bool includeDeps) { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); + auto* profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; @@ -220,27 +245,29 @@ void ModFolderPage::updateMods(bool includeDeps) QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedResources(selection); - bool use_all = mods_list.empty(); - if (use_all) - mods_list = m_model->allResources(); + auto modsList = m_model->selectedResources(selection); + bool useAll = modsList.empty(); + if (useAll) { + modsList = m_model->allResources(); + } - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, profile->getModLoadersList()); - update_dialog.checkCandidates(); + ResourceUpdateDialog updateDialog(this, m_instance, m_model, modsList, includeDeps, profile->getModLoadersList()); + updateDialog.checkCandidates(); - if (update_dialog.aborted()) { + if (updateDialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); return; } - if (update_dialog.noUpdates()) { - QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; - if (mods_list.size() > 1) { - if (use_all) { + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (useAll) { message = tr("All mods are up-to-date! :)"); } else { message = tr("All selected mods are up-to-date! :)"); @@ -250,9 +277,9 @@ void ModFolderPage::updateMods(bool includeDeps) return; } - if (update_dialog.exec()) { - auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -268,7 +295,7 @@ void ModFolderPage::updateMods(bool includeDeps) tasks->deleteLater(); }); - for (auto task : update_dialog.getTasks()) { + for (const auto& task : updateDialog.getTasks()) { tasks->addTask(task); } @@ -284,8 +311,9 @@ void ModFolderPage::deleteModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedMods(selection).length(); - if (selectionCount == 0) + if (selectionCount == 0) { return; + } if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 mods.\n" @@ -294,8 +322,9 @@ void ModFolderPage::deleteModMetadata() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } m_model->deleteMetadata(selection); @@ -303,10 +332,11 @@ void ModFolderPage::deleteModMetadata() void ModFolderPage::changeModVersion() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); + auto* profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; @@ -317,15 +347,16 @@ void ModFolderPage::changeModVersion() return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); - if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) + auto modsList = m_model->selectedMods(selection); + if (modsList.length() != 1 || modsList[0]->metadata() == nullptr) { return; + } - m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance, true); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); - m_downloadDialog->setResourceMetadata((*mods_list.begin())->metadata()); + m_downloadDialog->setResourceMetadata((*modsList.begin())->metadata()); m_downloadDialog->open(); } @@ -333,21 +364,21 @@ void ModFolderPage::exportModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectedMods = m_model->selectedMods(selection); - if (selectedMods.length() == 0) + if (selectedMods.length() == 0) { selectedMods = m_model->allMods(); + } - std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); + std::ranges::sort(selectedMods, [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); ExportToModListDialog dlg(m_instance->name(), selectedMods, this); dlg.exec(); } -CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) +CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent) { - auto mcInst = dynamic_cast(m_instance); + auto* mcInst = dynamic_cast(m_instance); if (mcInst) { - auto version = mcInst->getPackProfile(); - if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { + auto* version = mcInst->getPackProfile(); + if ((version != nullptr) && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { auto minecraftCmp = version->getComponent("net.minecraft"); if (!minecraftCmp->m_loaded) { version->reload(Net::Mode::Offline); @@ -370,22 +401,22 @@ CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr(m_instance); - if (!inst) + auto* inst = dynamic_cast(m_instance); + if (!inst) { return true; + } - auto version = inst->getPackProfile(); - if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) + auto* version = inst->getPackProfile(); + if ((version == nullptr) || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) { return false; + } auto minecraftCmp = version->getComponent("net.minecraft"); return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; } return false; } -NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) -{} +NilModFolderPage::NilModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent) {} bool NilModFolderPage::shouldDisplay() const { @@ -395,31 +426,22 @@ bool NilModFolderPage::shouldDisplay() const // Helper function so this doesn't need to be duplicated 3 times inline bool ModFolderPage::handleNoModLoader() { - int resp = - QMessageBox::question(this, this->tr("Missing Mod Loader"), - this->tr("You need to install a compatible mod loader before installing mods. Would you like to do so?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - switch (resp) { - case QMessageBox::Yes: { - // Should be safe - auto profile = static_cast(this->m_instance)->getPackProfile(); - InstallLoaderDialog dialog(profile, QString(), this); - bool ret = dialog.exec(); - this->m_container->refreshContainer(); + int resp = QMessageBox::question( + this, ModFolderPage::tr("Missing Mod Loader"), + ModFolderPage::tr("You need to install a compatible mod loader before installing mods. Would you like to do so?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (resp == QMessageBox::Yes) { + // Should be safe + auto* profile = static_cast(this->m_instance)->getPackProfile(); + InstallLoaderDialog dialog(profile, QString(), this); + bool ret = dialog.exec() != 0; + this->m_container->refreshContainer(); - // returning negation of dialog.exec which'll be true if the install loader dialog got canceled/closed - // and false if the user went through and installed a loader - return !ret; - } - case QMessageBox::No: { - // Nothing happens the dialog is already closing - // returning true so the caller doesn't go and continue with opening it's dialog without a mod loader - return true; - } - default: { - // Unreachable - // returning true as a safety measure - return true; - } + // returning negation of dialog.exec which'll be true if the install loader dialog got canceled/closed + // and false if the user went through and installed a loader + return !ret; } + // Nothing happens the dialog is already closing + // returning true so the caller doesn't go and continue with opening it's dialog without a mod loader + return true; } diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index aadeecb20..62db9fad8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -48,7 +48,7 @@ class ModFolderPage : public ExternalResourcesPage { inline bool handleNoModLoader(); public: - explicit ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent = nullptr); + explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); virtual ~ModFolderPage() = default; void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } @@ -74,14 +74,14 @@ class ModFolderPage : public ExternalResourcesPage { void changeModVersion(); protected: - std::shared_ptr m_model; + ModFolderModel* m_model; QPointer m_downloadDialog; }; class CoreModFolderPage : public ModFolderPage { Q_OBJECT public: - explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); + explicit CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent = 0); virtual ~CoreModFolderPage() = default; virtual QString displayName() const override { return tr("Core Mods"); } @@ -95,7 +95,7 @@ class CoreModFolderPage : public ModFolderPage { class NilModFolderPage : public ModFolderPage { Q_OBJECT public: - explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); + explicit NilModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent = 0); virtual ~NilModFolderPage() = default; virtual QString displayName() const override { return tr("Nilmods"); } diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index a3914832b..b9f943777 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -50,7 +50,7 @@ #include #include -OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance, QWidget* parent) +OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, BaseInstance* instance, QWidget* parent) : QWidget(parent) , m_id(id) , m_displayName(displayName) @@ -64,10 +64,10 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, m_proxy = new LogFormatProxyModel(this); if (m_instance) { - m_model.reset(new LogModel(this)); + m_model = new LogModel(this); ui->trackLogCheckbox->hide(); } else { - m_model = APPLICATION->logModel; + m_model = APPLICATION->logModel.get(); } // set up fonts in the log proxy @@ -90,7 +90,7 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, } else { modelStateToUI(); } - m_proxy->setSourceModel(m_model.get()); + m_proxy->setSourceModel(m_model); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); @@ -182,7 +182,7 @@ void OtherLogsPage::populateSelectLogBox() ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); if (!m_instance) - ui->selectLogBox->addItem("Current logs"); + ui->selectLogBox->addItem(tr("Current logs")); ui->selectLogBox->addItems(getPaths()); ui->selectLogBox->blockSignals(false); @@ -243,8 +243,8 @@ void OtherLogsPage::reload() if (m_instance) { setControlsEnabled(false); } else { - m_model = APPLICATION->logModel; - m_proxy->setSourceModel(m_model.get()); + m_model = APPLICATION->logModel.get(); + m_proxy->setSourceModel(m_model); ui->text->setModel(m_proxy); ui->text->scrollToBottom(); UIToModelState(); @@ -277,10 +277,15 @@ void OtherLogsPage::reload() MessageLevel last = MessageLevel::Unknown; auto handleLine = [this, &last](QString line) { - if (line.isEmpty()) + if (!line.isEmpty() && line.back() == '\n') { + line.resize(line.size() - 1); + } + if (!line.isEmpty() && line.back() == '\r') { + line.resize(line.size() - 1); + } + if (line.isEmpty()) { return false; - if (line.back() == '\n') - line = line.remove(line.size() - 1, 1); + } MessageLevel level = MessageLevel::Unknown; QString lineTemp = line; // don't edit out the time and level for clarity @@ -299,7 +304,7 @@ void OtherLogsPage::reload() ui->text->clear(); ui->text->setModel(nullptr); if (!m_instance) { - m_model.reset(new LogModel(this)); + m_model = new LogModel(this); m_model->setMaxLines(getConsoleMaxLines(APPLICATION->settings())); m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(APPLICATION->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); @@ -338,7 +343,7 @@ void OtherLogsPage::reload() ui->text->setModel(m_proxy); ui->text->scrollToBottom(); } else { - m_proxy->setSourceModel(m_model.get()); + m_proxy->setSourceModel(m_model); ui->text->setModel(m_proxy); ui->text->scrollToBottom(); UIToModelState(); @@ -472,14 +477,14 @@ void OtherLogsPage::setControlsEnabled(const bool enabled) ui->btnDelete->setEnabled(enabled); ui->btnClean->setEnabled(enabled); } else if (!m_currentFile.isEmpty()) { - ui->btnReload->setText("&Reload"); - ui->btnReload->setToolTip("Reload the contents of the log from the disk"); + ui->btnReload->setText(tr("&Reload")); + ui->btnReload->setToolTip(tr("Reload the contents of the log from the disk")); ui->btnDelete->setEnabled(enabled); ui->btnClean->setEnabled(enabled); ui->trackLogCheckbox->setEnabled(false); } else { - ui->btnReload->setText("Clear"); - ui->btnReload->setToolTip("Clear the log"); + ui->btnReload->setText(tr("Clear")); + ui->btnReload->setToolTip(tr("Clear the log")); ui->btnDelete->setEnabled(false); ui->btnClean->setEnabled(false); ui->trackLogCheckbox->setEnabled(enabled); diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 9fc0ba3b9..cd2fe6439 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -52,7 +52,7 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance = nullptr, QWidget* parent = 0); + explicit OtherLogsPage(QString id, QString displayName, QString helpPage, BaseInstance* instance = nullptr, QWidget* parent = 0); ~OtherLogsPage(); QString id() const override { return m_id; } @@ -97,7 +97,7 @@ class OtherLogsPage : public QWidget, public BasePage { QString m_helpPage; Ui::OtherLogsPage* ui; - InstancePtr m_instance; + BaseInstance* m_instance; /** Path to display log paths relative to. */ QString m_basePath; QStringList m_logSearchPaths; @@ -105,5 +105,5 @@ class OtherLogsPage : public QWidget, public BasePage { QFileSystemWatcher m_watcher; LogFormatProxyModel* m_proxy; - shared_qobject_ptr m_model; + LogModel* m_model; }; diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 2b5a5a86c..e4709ab2b 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -42,7 +42,7 @@ #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" -ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) +ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, ResourcePackFolderModel* model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); @@ -56,9 +56,9 @@ ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr< connect(ui->actionUpdateItem, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - auto updateMenu = new QMenu(this); + auto* updateMenu = new QMenu(this); - auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + auto* update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); updateMenu->addAction(ui->actionResetItemMetadata); @@ -75,14 +75,15 @@ void ResourcePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - auto& rp = static_cast(m_model->at(row)); + auto& rp = m_model->at(row); ui->frame->updateWithResourcePack(rp); } void ResourcePackPage::downloadResourcePacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); @@ -93,9 +94,9 @@ void ResourcePackPage::downloadResourcePacks() void ResourcePackPage::downloadDialogFinished(int result) { - if (result) { - auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (result != 0) { + auto* tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -105,8 +106,9 @@ void ResourcePackPage::downloadDialogFinished(int result) }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -125,16 +127,17 @@ void ResourcePackPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) + if (m_downloadDialog) { m_downloadDialog->deleteLater(); + } } void ResourcePackPage::updateResourcePacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); return; @@ -148,27 +151,29 @@ void ResourcePackPage::updateResourcePacks() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedResources(selection); - bool use_all = mods_list.empty(); - if (use_all) - mods_list = m_model->allResources(); + auto modsList = m_model->selectedResources(selection); + bool useAll = modsList.empty(); + if (useAll) { + modsList = m_model->allResources(); + } - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); - update_dialog.checkCandidates(); + ResourceUpdateDialog updateDialog(this, m_instance, m_model, modsList, false); + updateDialog.checkCandidates(); - if (update_dialog.aborted()) { + if (updateDialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The resource pack updater was aborted!"), QMessageBox::Warning)->show(); return; } - if (update_dialog.noUpdates()) { - QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; - if (mods_list.size() > 1) { - if (use_all) { + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (useAll) { message = tr("All resource packs are up-to-date! :)"); } else { message = tr("All selected resource packs are up-to-date! :)"); @@ -178,9 +183,9 @@ void ResourcePackPage::updateResourcePacks() return; } - if (update_dialog.exec()) { - auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -196,7 +201,7 @@ void ResourcePackPage::updateResourcePacks() tasks->deleteLater(); }); - for (auto task : update_dialog.getTasks()) { + for (const auto& task : updateDialog.getTasks()) { tasks->addTask(task); } @@ -212,8 +217,9 @@ void ResourcePackPage::deleteResourcePackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedResourcePacks(selection).length(); - if (selectionCount == 0) + if (selectionCount == 0) { return; + } if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 resource packs.\n" @@ -222,8 +228,9 @@ void ResourcePackPage::deleteResourcePackMetadata() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } m_model->deleteMetadata(selection); @@ -231,8 +238,9 @@ void ResourcePackPage::deleteResourcePackMetadata() void ResourcePackPage::changeResourcePackVersion() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); @@ -241,15 +249,17 @@ void ResourcePackPage::changeResourcePackVersion() const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); - if (rows.count() != 1) + if (rows.count() != 1) { return; + } Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); - if (resource.metadata() == nullptr) + if (resource.metadata() == nullptr) { return; + } - m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance, true); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 0ad24fc45..4e673e98c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -48,7 +48,7 @@ class ResourcePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); + explicit ResourcePackPage(MinecraftInstance* instance, ResourcePackFolderModel* model, QWidget* parent = 0); QString displayName() const override { return tr("Resource Packs"); } QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } @@ -71,6 +71,6 @@ class ResourcePackPage : public ExternalResourcesPage { void changeResourcePackVersion(); protected: - std::shared_ptr m_model; + ResourcePackFolderModel* m_model; QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index dc0290e1b..71dc7218e 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -53,8 +52,11 @@ #include #include #include +#include +#include #include +#include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -68,14 +70,36 @@ #include #include "RWStorage.h" +class ScreenshotsFSModel : public QFileSystemModel { + public: + bool canDropMimeData(const QMimeData* data, + const Qt::DropAction action, + const int row, + const int column, + const QModelIndex& parent) const override + { + const QUrl root = QUrl::fromLocalFile(rootPath()); + // this disables reordering items inside the model + // by rejecting drops if the file is already inside the folder + if (data->hasUrls()) { + for (auto& url : data->urls()) { + if (root.isParentOf(url)) { + return false; + } + } + } + return QFileSystemModel::canDropMimeData(data, action, row, column, parent); + } +}; + using SharedIconCache = RWStorage; using SharedIconCachePtr = std::shared_ptr; class ThumbnailingResult : public QObject { Q_OBJECT public slots: - inline void emitResultsReady(const QString& path) { emit resultsReady(path); } - inline void emitResultsFailed(const QString& path) { emit resultsFailed(path); } + void emitResultsReady(const QString& path) { emit resultsReady(path); } + void emitResultsFailed(const QString& path) { emit resultsFailed(path); } signals: void resultsReady(const QString& path); void resultsFailed(const QString& path); @@ -83,32 +107,32 @@ class ThumbnailingResult : public QObject { class ThumbnailRunnable : public QRunnable { public: - ThumbnailRunnable(QString path, SharedIconCachePtr cache) + ThumbnailRunnable(QString path, SharedIconCachePtr cache) : m_path(std::move(path)), m_cache(std::move(cache)) {} + void run() override { - m_path = path; - m_cache = cache; - } - void run() - { - QFileInfo info(m_path); - if (info.isDir()) + const QFileInfo info(m_path); + if (info.isDir()) { return; - if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) + } + if (info.suffix().compare("png", Qt::CaseInsensitive) != 0) { return; - if (!m_cache->stale(m_path)) + } + if (!m_cache->stale(m_path)) { return; - QImage image(m_path); + } + const QImage image(m_path); if (image.isNull()) { m_resultEmitter.emitResultsFailed(m_path); qDebug() << "Error loading screenshot (perhaps too large?):" + m_path; return; } QImage small; - if (image.width() > image.height()) + if (image.width() > image.height()) { small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); - else + } else { small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); - QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + } + const QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); QImage square(QSize(256, 256), QImage::Format_ARGB32); square.fill(Qt::transparent); @@ -116,7 +140,7 @@ class ThumbnailRunnable : public QRunnable { painter.drawImage(offset, small); painter.end(); - QIcon icon(QPixmap::fromImage(square)); + const QIcon icon(QPixmap::fromImage(square)); m_cache->add(m_path, icon); m_resultEmitter.emitResultsReady(m_path); } @@ -130,59 +154,62 @@ class ThumbnailRunnable : public QRunnable { class FilterModel : public QIdentityProxyModel { Q_OBJECT public: - explicit FilterModel(QObject* parent = 0) : QIdentityProxyModel(parent) + explicit FilterModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) { m_thumbnailingPool.setMaxThreadCount(4); m_thumbnailCache = std::make_shared(); m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder")); connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged); } - virtual ~FilterModel() + ~FilterModel() override { m_thumbnailingPool.clear(); - if (!m_thumbnailingPool.waitForDone(500)) + if (!m_thumbnailingPool.waitForDone(500)) { qDebug() << "Thumbnail pool took longer than 500ms to finish"; + } } - virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const + QVariant data(const QModelIndex& proxyIndex, const int role = Qt::DisplayRole) const override // NOLINT(*-default-arguments) { - auto model = sourceModel(); - if (!model) - return QVariant(); + const auto* model = sourceModel(); + if (!model) { + return {}; + } if (role == Qt::DisplayRole || role == Qt::EditRole) { - QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); + const QVariant result = model->data(mapToSource(proxyIndex), role); static const QRegularExpression s_removeChars("\\.png$"); return result.toString().remove(s_removeChars); } if (role == Qt::DecorationRole) { - QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); - QString filePath = result.toString(); - QIcon temp; + const QVariant result = model->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); + const QString filePath = result.toString(); if (!watched.contains(filePath)) { - ((QFileSystemWatcher&)watcher).addPath(filePath); - ((QSet&)watched).insert(filePath); + const_cast(watcher).addPath(filePath); + const_cast&>(watched).insert(filePath); } - if (m_thumbnailCache->get(filePath, temp)) { + if (QIcon temp; m_thumbnailCache->get(filePath, temp)) { return temp; } if (!m_failed.contains(filePath)) { - ((FilterModel*)this)->thumbnailImage(filePath); + const_cast(this)->thumbnailImage(filePath); } return (m_thumbnailCache->get("placeholder")); } - return sourceModel()->data(mapToSource(proxyIndex), role); + return model->data(mapToSource(proxyIndex), role); } - virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) + bool setData(const QModelIndex& index, const QVariant& value, const int role = Qt::EditRole) override // NOLINT(*-default-arguments) { - auto model = sourceModel(); - if (!model) + auto* model = sourceModel(); + if (!model) { return false; - if (role != Qt::EditRole) + } + if (role != Qt::EditRole) { return false; + } // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't // sort after renames { - ((QFileSystemModel*)model)->setNameFilterDisables(true); - ((QFileSystemModel*)model)->setNameFilterDisables(false); + static_cast(model)->setNameFilterDisables(true); + static_cast(model)->setNameFilterDisables(false); } return model->setData(mapToSource(index), value.toString() + ".png", role); } @@ -190,15 +217,15 @@ class FilterModel : public QIdentityProxyModel { private: void thumbnailImage(QString path) { - auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); + auto* runnable = new ThumbnailRunnable(std::move(path), m_thumbnailCache); connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsReady, this, &FilterModel::thumbnailReady); connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsFailed, this, &FilterModel::thumbnailFailed); m_thumbnailingPool.start(runnable); } private slots: - void thumbnailReady(QString path) { emit layoutChanged(); } - void thumbnailFailed(QString path) { m_failed.insert(path); } - void fileChanged(QString filepath) + void thumbnailReady(const QString& /*path*/) { emit layoutChanged(); } + void thumbnailFailed(const QString& path) { m_failed.insert(path); } + void fileChanged(const QString& filepath) { m_thumbnailCache->setStale(filepath); // reinsert the path... @@ -219,13 +246,12 @@ class FilterModel : public QIdentityProxyModel { class CenteredEditingDelegate : public QStyledItemDelegate { public: - explicit CenteredEditingDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) {} - virtual ~CenteredEditingDelegate() {} - virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const + explicit CenteredEditingDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + ~CenteredEditingDelegate() override = default; + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override { - auto widget = QStyledItemDelegate::createEditor(parent, option, index); - auto foo = dynamic_cast(widget); - if (foo) { + auto* widget = QStyledItemDelegate::createEditor(parent, option, index); + if (auto* foo = dynamic_cast(widget)) { foo->setAlignment(Qt::AlignHCenter); foo->setFrame(true); foo->setMaximumWidth(192); @@ -234,10 +260,11 @@ class CenteredEditingDelegate : public QStyledItemDelegate { } }; -ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(parent), ui(new Ui::ScreenshotsPage) +ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) + : QMainWindow(parent), ui(new Ui::ScreenshotsPage), m_folder(std::move(path)) { - m_model.reset(new QFileSystemModel()); - m_filterModel.reset(new FilterModel()); + m_model = std::make_shared(); + m_filterModel = std::make_shared(); m_filterModel->setSourceModel(m_model.get()); m_model->setFilter(QDir::Files); m_model->setReadOnly(false); @@ -248,7 +275,6 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa constexpr int file_modified_column_index = 3; m_model->sort(file_modified_column_index, Qt::DescendingOrder); - m_folder = path; m_valid = FS::ensureFolderPathExists(m_folder); ui->setupUi(this); @@ -265,18 +291,19 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); + connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::showContextMenu); connect(ui->listView, &QAbstractItemView::activated, this, &ScreenshotsPage::onItemActivated); } bool ScreenshotsPage::eventFilter(QObject* obj, QEvent* evt) { - if (obj != ui->listView) + if (obj != ui->listView) { return QWidget::eventFilter(obj, evt); + } if (evt->type() != QEvent::KeyPress) { return QWidget::eventFilter(obj, evt); } - QKeyEvent* keyEvent = static_cast(evt); + const auto* keyEvent = static_cast(evt); if (keyEvent->matches(QKeySequence::Copy)) { on_actionCopy_File_s_triggered(); @@ -306,11 +333,11 @@ ScreenshotsPage::~ScreenshotsPage() delete ui; } -void ScreenshotsPage::ShowContextMenu(const QPoint& pos) +void ScreenshotsPage::showContextMenu(const QPoint& pos) { - auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + auto* menu = ui->toolBar->createContextMenu(this, tr("Context menu")); - if (ui->listView->selectionModel()->selectedRows().size() > 1) { + if (ui->listView->selectionModel()->selectedIndexes().size() > 1) { menu->removeAction(ui->actionCopy_Image); } @@ -325,66 +352,75 @@ QMenu* ScreenshotsPage::createPopupMenu() return filteredMenu; } -void ScreenshotsPage::onItemActivated(QModelIndex index) +void ScreenshotsPage::onItemActivated(QModelIndex index) const { - if (!index.isValid()) + if (!index.isValid()) { return; - auto info = m_model->fileInfo(index); + } + const auto info = m_model->fileInfo(index); DesktopServices::openPath(info); } -void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) +void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& /*selected*/) const { + const auto selected = ui->listView->selectionModel()->selectedIndexes(); + bool allReadable = !selected.isEmpty(); bool allWritable = !selected.isEmpty(); - for (auto index : selected.indexes()) { - if (!index.isValid()) + for (auto index : selected) { + if (!index.isValid()) { break; + } auto info = m_model->fileInfo(index); - if (!info.isReadable()) + if (!info.isReadable()) { allReadable = false; - if (!info.isWritable()) + } + if (!info.isWritable()) { allWritable = false; + } } ui->actionUpload->setEnabled(allReadable); - ui->actionCopy_Image->setEnabled(allReadable); + ui->actionCopy_Image->setEnabled(allReadable && selected.size() == 1); ui->actionCopy_File_s->setEnabled(allReadable); ui->actionDelete->setEnabled(allWritable); ui->actionRename->setEnabled(allWritable); } -void ScreenshotsPage::on_actionView_Folder_triggered() +void ScreenshotsPage::on_actionView_Folder_triggered() const { DesktopServices::openPath(m_folder, true); } void ScreenshotsPage::on_actionUpload_triggered() { - auto selection = ui->listView->selectionModel()->selectedRows(); - if (selection.isEmpty()) + auto selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.isEmpty()) { return; + } QString text; - QUrl baseUrl(BuildConfig.IMGUR_BASE_URL); - if (selection.size() > 1) + const QUrl baseUrl(BuildConfig.IMGUR_BASE_URL); + if (selection.size() > 1) { text = tr("You are about to upload %1 screenshots to %2.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(QString::number(selection.size()), baseUrl.host()); - else + } else { text = tr("You are about to upload the selected screenshot to %1.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(baseUrl.host()); + } auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } QList uploaded; auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); @@ -398,7 +434,7 @@ void ScreenshotsPage::on_actionUpload_triggered() auto screenshot = std::make_shared(info); job->addNetAction(ImgurUpload::make(screenshot)); - connect(job.get(), &Task::failed, [this](QString reason) { + connect(job.get(), &Task::failed, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &Task::aborted, [this] { @@ -439,7 +475,7 @@ void ScreenshotsPage::on_actionUpload_triggered() task.addTask(job); task.addTask(albumTask); - connect(&task, &Task::failed, [this](QString reason) { + connect(&task, &Task::failed, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show(); }); connect(&task, &Task::aborted, [this] { @@ -467,24 +503,24 @@ void ScreenshotsPage::on_actionUpload_triggered() m_uploadActive = false; } -void ScreenshotsPage::on_actionCopy_Image_triggered() +void ScreenshotsPage::on_actionCopy_Image_triggered() const { - auto selection = ui->listView->selectionModel()->selectedRows(); + auto selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() < 1) { return; } // You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied. - auto item = selection[0]; - auto info = m_model->fileInfo(item); - QImage image(info.absoluteFilePath()); + const auto item = selection.first(); + const auto info = m_model->fileInfo(item); + const QImage image(info.absoluteFilePath()); Q_ASSERT(!image.isNull()); QApplication::clipboard()->setImage(image, QClipboard::Clipboard); } -void ScreenshotsPage::on_actionCopy_File_s_triggered() +void ScreenshotsPage::on_actionCopy_File_s_triggered() const { - auto selection = ui->listView->selectionModel()->selectedRows(); + auto selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() < 1) { // Don't do anything so we don't empty the users clipboard return; @@ -495,7 +531,7 @@ void ScreenshotsPage::on_actionCopy_File_s_triggered() auto info = m_model->fileInfo(item); buf += "file:///" + info.absoluteFilePath() + "\r\n"; } - QMimeData* mimeData = new QMimeData(); + auto* mimeData = new QMimeData(); mimeData->setData("text/uri-list", buf.toLocal8Bit()); QApplication::clipboard()->setMimeData(mimeData); } @@ -504,39 +540,43 @@ void ScreenshotsPage::on_actionDelete_triggered() { auto selected = ui->listView->selectionModel()->selectedIndexes(); - int count = ui->listView->selectionModel()->selectedRows().size(); + const qsizetype count = selected.size(); QString text; - if (count > 1) + if (count > 1) { text = tr("You are about to delete %1 screenshots.\n" "This may be permanent and they will be gone from the folder.\n\n" "Are you sure?") .arg(count); - else - text = tr("You are about to delete the selected screenshot.\n" - "This may be permanent and it will be gone from the folder.\n\n" - "Are you sure?") - .arg(count); + } else { + text = + tr("You are about to delete the selected screenshot.\n" + "This may be permanent and it will be gone from the folder.\n\n" + "Are you sure?"); + } - auto response = + const auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } for (auto item : selected) { - if (FS::trash(m_model->filePath(item))) + if (FS::trash(m_model->filePath(item))) { continue; + } m_model->remove(item); } } -void ScreenshotsPage::on_actionRename_triggered() +void ScreenshotsPage::on_actionRename_triggered() const { auto selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.isEmpty()) + if (selection.isEmpty()) { return; - ui->listView->edit(selection[0]); + } + ui->listView->edit(selection.first()); // TODO: mass renaming } @@ -546,8 +586,8 @@ void ScreenshotsPage::openedImpl() m_valid = FS::ensureFolderPathExists(m_folder); } if (m_valid) { - QString path = QDir(m_folder).absolutePath(); - auto idx = m_model->setRootPath(path); + const QString path = QDir(m_folder).absolutePath(); + const auto idx = m_model->setRootPath(path); if (idx.isValid()) { ui->listView->setModel(m_filterModel.get()); connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, @@ -559,7 +599,7 @@ void ScreenshotsPage::openedImpl() } } - auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + const auto setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index b9c750a1f..7d1cf4fcc 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -41,13 +41,14 @@ #include "settings/Setting.h" -class QFileSystemModel; class QIdentityProxyModel; class QItemSelection; namespace Ui { class ScreenshotsPage; } +class ScreenshotsFSModel; + struct ScreenShot; class ScreenshotList; class ImgurAlbumCreation; @@ -77,18 +78,18 @@ class ScreenshotsPage : public QMainWindow, public BasePage { private slots: void on_actionUpload_triggered(); - void on_actionCopy_Image_triggered(); - void on_actionCopy_File_s_triggered(); + void on_actionCopy_Image_triggered() const; + void on_actionCopy_File_s_triggered() const; void on_actionDelete_triggered(); - void on_actionRename_triggered(); - void on_actionView_Folder_triggered(); - void onItemActivated(QModelIndex); - void onCurrentSelectionChanged(const QItemSelection& selected); - void ShowContextMenu(const QPoint& pos); + void on_actionRename_triggered() const; + void on_actionView_Folder_triggered() const; + void onItemActivated(QModelIndex) const; + void onCurrentSelectionChanged(const QItemSelection& selected) const; + void showContextMenu(const QPoint& pos); private: Ui::ScreenshotsPage* ui; - std::shared_ptr m_model; + std::shared_ptr m_model; std::shared_ptr m_filterModel; QString m_folder; bool m_valid = false; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui index db55869cd..4ed92c4b6 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.ui +++ b/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -33,10 +33,13 @@ QAbstractItemView::SelectionMode::ExtendedSelection - QAbstractItemView::SelectionBehavior::SelectRows + QAbstractItemView::SelectionBehavior::SelectItems + + + QAbstractItemView::ScrollMode::ScrollPerPixel - QListView::Movement::Static + QListView::Movement::Snap diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 59ae6e43f..9012ebd5b 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -548,7 +548,7 @@ class ServersModel : public QAbstractListModel { ConcurrentTask::Ptr m_currentQueryTask = nullptr; }; -ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) +ServersPage::ServersPage(BaseInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) { ui->setupUi(this); m_inst = inst; @@ -568,7 +568,7 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent auto selectionModel = ui->serversView->selectionModel(); connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); - connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged); + connect(m_inst, &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged); connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); connect(ui->resourceComboBox, &QComboBox::currentIndexChanged, this, &ServersPage::resourceIndexChanged); @@ -757,7 +757,7 @@ void ServersPage::on_actionMove_Down_triggered() void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; - APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); + APPLICATION->launch(m_inst, LaunchMode::Normal, std::make_shared(MinecraftTarget::parse(address, false))); } void ServersPage::on_actionRefresh_triggered() diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 49a746245..0eec57de1 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -56,7 +56,7 @@ class ServersPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit ServersPage(InstancePtr inst, QWidget* parent = 0); + explicit ServersPage(BaseInstance* inst, QWidget* parent = 0); virtual ~ServersPage(); void openedImpl() override; @@ -100,7 +100,7 @@ class ServersPage : public QMainWindow, public BasePage { bool m_locked = true; Ui::ServersPage* ui = nullptr; ServersModel* m_model = nullptr; - InstancePtr m_inst = nullptr; + BaseInstance* m_inst = nullptr; std::shared_ptr m_wide_bar_setting = nullptr; }; diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui index d330835c8..727b64e49 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -53,6 +53,9 @@ false + + QAbstractItemView::ScrollPerPixel + false diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index baf18da52..a29564abc 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -47,7 +47,7 @@ #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" -ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) +ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, ShaderPackFolderModel* model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); @@ -61,9 +61,9 @@ ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptractionUpdateItem, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - auto updateMenu = new QMenu(this); + auto* updateMenu = new QMenu(this); - auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + auto* update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); updateMenu->addAction(ui->actionResetItemMetadata); @@ -78,8 +78,9 @@ ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptrtypeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); @@ -90,9 +91,9 @@ void ShaderPackPage::downloadShaderPack() void ShaderPackPage::downloadDialogFinished(int result) { - if (result) { - auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (result != 0) { + auto* tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -102,8 +103,9 @@ void ShaderPackPage::downloadDialogFinished(int result) }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -122,16 +124,17 @@ void ShaderPackPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) + if (m_downloadDialog) { m_downloadDialog->deleteLater(); + } } void ShaderPackPage::updateShaderPacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); return; @@ -145,27 +148,29 @@ void ShaderPackPage::updateShaderPacks() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedResources(selection); - bool use_all = mods_list.empty(); - if (use_all) - mods_list = m_model->allResources(); + auto modsList = m_model->selectedResources(selection); + bool useAll = modsList.empty(); + if (useAll) { + modsList = m_model->allResources(); + } - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); - update_dialog.checkCandidates(); + ResourceUpdateDialog updateDialog(this, m_instance, m_model, modsList, false); + updateDialog.checkCandidates(); - if (update_dialog.aborted()) { + if (updateDialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The shader pack updater was aborted!"), QMessageBox::Warning)->show(); return; } - if (update_dialog.noUpdates()) { - QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; - if (mods_list.size() > 1) { - if (use_all) { + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (useAll) { message = tr("All shader packs are up-to-date! :)"); } else { message = tr("All selected shader packs are up-to-date! :)"); @@ -175,9 +180,9 @@ void ShaderPackPage::updateShaderPacks() return; } - if (update_dialog.exec()) { - auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -193,7 +198,7 @@ void ShaderPackPage::updateShaderPacks() tasks->deleteLater(); }); - for (auto task : update_dialog.getTasks()) { + for (const auto& task : updateDialog.getTasks()) { tasks->addTask(task); } @@ -209,8 +214,9 @@ void ShaderPackPage::deleteShaderPackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedShaderPacks(selection).length(); - if (selectionCount == 0) + if (selectionCount == 0) { return; + } if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 shader packs.\n" @@ -219,8 +225,9 @@ void ShaderPackPage::deleteShaderPackMetadata() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } m_model->deleteMetadata(selection); @@ -228,8 +235,9 @@ void ShaderPackPage::deleteShaderPackMetadata() void ShaderPackPage::changeShaderPackVersion() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); @@ -238,15 +246,17 @@ void ShaderPackPage::changeShaderPackVersion() const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); - if (rows.count() != 1) + if (rows.count() != 1) { return; + } Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); - if (resource.metadata() == nullptr) + if (resource.metadata() == nullptr) { return; + } - m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance, true); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index c6ae3bc24..cc53a01e1 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -44,7 +44,7 @@ class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + explicit ShaderPackPage(MinecraftInstance* instance, ShaderPackFolderModel* model, QWidget* parent = nullptr); ~ShaderPackPage() override = default; QString displayName() const override { return tr("Shader Packs"); } @@ -62,6 +62,6 @@ class ShaderPackPage : public ExternalResourcesPage { void changeShaderPackVersion(); private: - std::shared_ptr m_model; + ShaderPackFolderModel* m_model; QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 163a14c86..01325e3f6 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -46,7 +46,7 @@ #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" -TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) +TexturePackPage::TexturePackPage(MinecraftInstance* instance, TexturePackFolderModel* model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); @@ -60,9 +60,9 @@ TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptractionUpdateItem, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - auto updateMenu = new QMenu(this); + auto* updateMenu = new QMenu(this); - auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + auto* update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); updateMenu->addAction(ui->actionResetItemMetadata); @@ -81,14 +81,15 @@ void TexturePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] c { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - auto& rp = static_cast(m_model->at(row)); + auto& rp = m_model->at(row); ui->frame->updateWithTexturePack(rp); } void TexturePackPage::downloadTexturePacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); @@ -98,9 +99,9 @@ void TexturePackPage::downloadTexturePacks() void TexturePackPage::downloadDialogFinished(int result) { - if (result) { - auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (result != 0) { + auto* tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -110,8 +111,9 @@ void TexturePackPage::downloadDialogFinished(int result) }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); - if (warnings.count()) + if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } tasks->deleteLater(); }); @@ -130,16 +132,17 @@ void TexturePackPage::downloadDialogFinished(int result) m_model->update(); } - if (m_downloadDialog) + if (m_downloadDialog) { m_downloadDialog->deleteLater(); + } } void TexturePackPage::updateTexturePacks() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } - auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); return; @@ -153,27 +156,29 @@ void TexturePackPage::updateTexturePacks() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedResources(selection); - bool use_all = mods_list.empty(); - if (use_all) - mods_list = m_model->allResources(); + auto modsList = m_model->selectedResources(selection); + bool useAll = modsList.empty(); + if (useAll) { + modsList = m_model->allResources(); + } - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); - update_dialog.checkCandidates(); + ResourceUpdateDialog updateDialog(this, m_instance, m_model, modsList, false); + updateDialog.checkCandidates(); - if (update_dialog.aborted()) { + if (updateDialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The texture pack updater was aborted!"), QMessageBox::Warning)->show(); return; } - if (update_dialog.noUpdates()) { - QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; - if (mods_list.size() > 1) { - if (use_all) { + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (useAll) { message = tr("All texture packs are up-to-date! :)"); } else { message = tr("All selected texture packs are up-to-date! :)"); @@ -183,9 +188,9 @@ void TexturePackPage::updateTexturePacks() return; } - if (update_dialog.exec()) { - auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); @@ -201,7 +206,7 @@ void TexturePackPage::updateTexturePacks() tasks->deleteLater(); }); - for (auto task : update_dialog.getTasks()) { + for (const auto& task : updateDialog.getTasks()) { tasks->addTask(task); } @@ -217,8 +222,9 @@ void TexturePackPage::deleteTexturePackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedTexturePacks(selection).length(); - if (selectionCount == 0) + if (selectionCount == 0) { return; + } if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 texture packs.\n" @@ -227,8 +233,9 @@ void TexturePackPage::deleteTexturePackMetadata() QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) { return; + } } m_model->deleteMetadata(selection); @@ -236,8 +243,9 @@ void TexturePackPage::deleteTexturePackMetadata() void TexturePackPage::changeTexturePackVersion() { - if (m_instance->typeName() != "Minecraft") + if (m_instance->typeName() != "Minecraft") { return; // this is a null instance or a legacy instance + } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); @@ -246,15 +254,17 @@ void TexturePackPage::changeTexturePackVersion() const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); - if (rows.count() != 1) + if (rows.count() != 1) { return; + } Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); - if (resource.metadata() == nullptr) + if (resource.metadata() == nullptr) { return; + } - m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance, true); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 2c92212b9..dad0affa4 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -48,7 +48,7 @@ class TexturePackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + explicit TexturePackPage(MinecraftInstance* instance, TexturePackFolderModel* model, QWidget* parent = nullptr); QString displayName() const override { return tr("Texture packs"); } QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } @@ -66,6 +66,6 @@ class TexturePackPage : public ExternalResourcesPage { void changeTexturePackVersion(); private: - std::shared_ptr m_model; + TexturePackFolderModel* m_model; QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index ef5427a00..fea759bb2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -151,7 +151,7 @@ VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow reloadPackProfile(); auto proxy = new IconProxy(ui->packageView); - proxy->setSourceModel(m_profile.get()); + proxy->setSourceModel(m_profile); m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); @@ -168,7 +168,7 @@ VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow auto smodel = ui->packageView->selectionModel(); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); - connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); + connect(m_profile, &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); updateVersionControls(); preselect(0); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 602d09206..493e3b8c8 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -105,7 +105,7 @@ class VersionPage : public QMainWindow, public BasePage { private: Ui::VersionPage* ui; QSortFilterProxyModel* m_filterModel; - std::shared_ptr m_profile; + PackProfile* m_profile; MinecraftInstance* m_inst; int currentIdx = 0; diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index c7eaf94a0..71c370ab2 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -86,7 +86,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent) +WorldListPage::WorldListPage(MinecraftInstance* inst, WorldList* worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); @@ -95,7 +95,7 @@ WorldListPage::WorldListPage(MinecraftInstancePtr inst, std::shared_ptrsetSortCaseSensitivity(Qt::CaseInsensitive); - proxy->setSourceModel(m_worlds.get()); + proxy->setSourceModel(m_worlds); proxy->setSortRole(Qt::UserRole); ui->worldTreeView->setSortingEnabled(true); ui->worldTreeView->setModel(proxy); @@ -117,12 +117,11 @@ void WorldListPage::openedImpl() { m_worlds->startWatching(); - auto mInst = std::dynamic_pointer_cast(m_inst); - if (!mInst || !mInst->traits().contains("feature:is_quick_play_singleplayer")) { + if (!m_inst || !m_inst->traits().contains("feature:is_quick_play_singleplayer")) { ui->toolBar->removeAction(ui->actionJoin); } - auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + const auto setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); @@ -237,11 +236,10 @@ void WorldListPage::on_actionData_Packs_triggered() GenericPageProvider provider(dialog->windowTitle()); - provider.addPageCreator([this, folder] { - bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); - return new DataPackPage(m_inst.get(), std::move(model)); - }); + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_datapackModel.reset(new DataPackFolderModel(folder, m_inst, isIndexed, true)); + + provider.addPageCreator([this] { return new DataPackPage(m_inst, m_datapackModel.get(), this); }); auto layout = new QVBoxLayout(dialog); @@ -261,9 +259,12 @@ void WorldListPage::on_actionData_Packs_triggered() dialog->setLayout(layout); - dialog->exec(); + dialog->setAttribute(Qt::WA_DeleteOnClose); - APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); + connect(dialog, &QDialog::finished, this, + [dialog]() { APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); }); + + dialog->open(); } void WorldListPage::on_actionReset_Icon_triggered() @@ -380,8 +381,7 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); - auto mInst = std::dynamic_pointer_cast(m_inst); - auto supportsJoin = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + auto supportsJoin = m_inst && m_inst->traits().contains("feature:is_quick_play_singleplayer"); ui->actionJoin->setEnabled(enable && supportsJoin); if (!supportsJoin) { @@ -474,7 +474,7 @@ void WorldListPage::on_actionJoin_triggered() } auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); auto world = (World*)worldVariant.value(); - APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); + APPLICATION->launch(m_inst, LaunchMode::Normal, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); } #include "WorldListPage.moc" diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 9b931066d..0afb9883b 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -52,7 +52,7 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); + explicit WorldListPage(MinecraftInstance* inst, WorldList* worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } @@ -71,7 +71,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - MinecraftInstancePtr m_inst; + MinecraftInstance* m_inst; private: QModelIndex getSelectedWorld(); @@ -81,11 +81,12 @@ class WorldListPage : public QMainWindow, public BasePage { private: Ui::WorldListPage* ui; - std::shared_ptr m_worlds; + WorldList* m_worlds; unique_qobject_ptr m_mceditProcess; bool m_mceditStarting = false; std::shared_ptr m_wide_bar_setting = nullptr; + std::unique_ptr m_datapackModel; private slots: void on_actionCopy_Seed_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 22c93256c..2c19b9783 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -53,6 +53,9 @@ true + + QAbstractItemView::ScrollPerPixel + false diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index 87e126fd7..f24abf9fb 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -80,14 +80,14 @@ void CustomPage::openedImpl() void CustomPage::refresh() { - ui->versionList->loadList(); + ui->versionList->loadList(true); } void CustomPage::loaderRefresh() { if (ui->noneFilter->isChecked()) return; - ui->loaderVersionList->loadList(); + ui->loaderVersionList->loadList(true); } void CustomPage::filterChanged() diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c43d3e1fa..6e783014f 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -131,10 +131,9 @@ void ImportPage::updateState() } auto addonId = query.allQueryItemValues("addonId")[0]; auto fileId = query.allQueryItemValues("fileId")[0]; - auto array = std::make_shared(); auto api = FlameAPI(); - auto job = api.getFile(addonId, fileId, array); + auto [job, array] = api.getFile(addonId, fileId); connect(job.get(), &NetJob::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index ef38a595d..c0768b9c3 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -27,7 +27,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() Q_ASSERT(profile); Q_ASSERT(m_filter); - std::optional> versions{}; + std::optional> versions{}; std::optional categories{}; auto loaders = profile->getSupportedModLoaders(); @@ -55,7 +55,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelInd Q_ASSERT(profile); Q_ASSERT(m_filter); - std::optional> versions{}; + std::optional> versions{}; auto loaders = profile->getSupportedModLoaders(); if (!m_filter->versions.empty()) versions = m_filter->versions; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 803ba6d5c..1cdce5e33 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -50,7 +50,6 @@ #include "ResourceDownloadTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" #include "ui/dialogs/ResourceDownloadDialog.h" @@ -63,15 +62,15 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePa void ModPage::setFilterWidget(std::unique_ptr& widget) { - if (m_filter_widget) + if (m_filter_widget) { disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); - - auto old = m_ui->splitter->replaceWidget(0, widget.get()); - // because we replaced the widget we also need to delete it - if (old) { - delete old; } + auto* old = m_ui->splitter->replaceWidget(0, widget.get()); + // because we replaced the widget we also need to delete it + + delete old; + m_filter_widget.swap(widget); m_filter = m_filter_widget->getFilter(); @@ -114,10 +113,11 @@ QMap ModPage::urlHandlers() const void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - const std::shared_ptr base_model) + ResourceFolderModel* baseModel, + QString downloadReason) { - bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_model->addPack(pack, version, base_model, is_indexed); + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(pack, version, baseModel, isIndexed, downloadReason); } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index d3b08cbd9..7f75995a3 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -29,10 +29,10 @@ class ModPage : public ResourcePage { static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); - auto model = static_cast(page->getModel()); + auto* model = static_cast(page->getModel()); - auto filter_widget = page->createFilterWidget(); - page->setFilterWidget(filter_widget); + auto filterWidget = page->createFilterWidget(); + page->setFilterWidget(filterWidget); model->setFilter(page->getFilter()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); @@ -43,18 +43,21 @@ class ModPage : public ResourcePage { } //: The plural version of 'mod' - inline QString resourcesString() const override { return tr("mods"); } + QString resourcesString() const override { return tr("mods"); } //: The singular version of 'mods' - inline QString resourceString() const override { return tr("mod"); } + QString resourceString() const override { return tr("mod"); } QMap urlHandlers() const override; - void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; + void addResourceToPage(ModPlatform::IndexedPack::Ptr /*unused*/, + ModPlatform::IndexedVersion& /*unused*/, + ResourceFolderModel* /*unused*/, + QString downloadReason = "standalone") override; virtual std::unique_ptr createFilterWidget() = 0; bool supportsFiltering() const override { return true; }; - auto getFilter() const -> const std::shared_ptr { return m_filter; } + auto getFilter() const -> std::shared_ptr { return m_filter; } void setFilterWidget(std::unique_ptr&); protected: diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.ui b/launcher/ui/pages/modplatform/OptionalModDialog.ui index 0b809d2cb..3ac9b5b13 100644 --- a/launcher/ui/pages/modplatform/OptionalModDialog.ui +++ b/launcher/ui/pages/modplatform/OptionalModDialog.ui @@ -24,6 +24,9 @@ true + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 30d3dbdf4..edd8564b6 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -12,9 +12,11 @@ #include #include #include +#include #include "Application.h" #include "BuildConfig.h" +#include "settings/SettingsObject.h" #include "modplatform/ResourceAPI.h" #include "net/ApiDownload.h" @@ -28,7 +30,7 @@ namespace ResourceDownload { QHash ResourceModel::s_running_models; -ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) +ResourceModel::ResourceModel(ResourceAPI* api) : m_api(api) { s_running_models.insert(this, true); if (APPLICATION_DYN) { @@ -61,14 +63,14 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant } case Qt::DecorationRole: { if (APPLICATION_DYN) { - if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); - icon_or_none.has_value()) - return icon_or_none.value(); + if (auto iconOrNone = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); + iconOrNone.has_value()) { + return iconOrNone.value(); + } return QIcon::fromTheme("screenshot-placeholder"); - } else { - return {}; } + return {}; } case Qt::SizeHintRole: return QSize(0, 58); @@ -111,8 +113,9 @@ QHash ResourceModel::roleNames() const bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); - if (pos >= m_packs.size() || pos < 0 || !index.isValid()) + if (pos >= m_packs.size() || pos < 0 || !index.isValid()) { return false; + } m_packs[pos] = value.value(); emit dataChanged(index, index); @@ -127,42 +130,51 @@ QString ResourceModel::debugName() const void ResourceModel::fetchMore(const QModelIndex& parent) { - if (parent.isValid() || m_search_state == SearchState::Finished) + if (parent.isValid() || m_search_state == SearchState::Finished) { return; + } search(); } void ResourceModel::search() { - if (hasActiveSearchJob()) + if (hasActiveSearchJob()) { return; + } - if (m_search_term.startsWith("#")) { + if (m_search_state != SearchState::ResetRequested && m_search_term.startsWith("#")) { auto projectId = m_search_term.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason, int) { - if (!s_running_models.constFind(this).value()) + callbacks.on_fail = [this](QString reason, int networkErrorCode) { + if (!s_running_models.constFind(this).value()) { return; - searchRequestFailed(reason, -1); + } + if (networkErrorCode == 404) { + m_search_state = SearchState::ResetRequested; + } + searchRequestFailed(std::move(reason), networkErrorCode); }; callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } searchRequestAborted(); }; callbacks.on_succeed = [this](auto& pack) { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } searchRequestForOneSucceeded(pack); }; auto project = std::make_shared(); project->addonId = projectId; - if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job) + if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks), false); job) { runSearchJob(job); + } return; } } @@ -171,31 +183,36 @@ void ResourceModel::search() ResourceAPI::Callback> callbacks{}; callbacks.on_succeed = [this](auto& doc) { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } searchRequestSucceeded(doc); }; - callbacks.on_fail = [this](QString reason, int network_error_code) { - if (!s_running_models.constFind(this).value()) + callbacks.on_fail = [this](QString reason, int networkErrorCode) { + if (!s_running_models.constFind(this).value()) { return; - searchRequestFailed(reason, network_error_code); + } + searchRequestFailed(std::move(reason), networkErrorCode); }; callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } searchRequestAborted(); }; - if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) + if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) { runSearchJob(job); + } } void ResourceModel::loadEntry(const QModelIndex& entry) { - auto const& pack = m_packs[entry.row()]; + const auto& pack = m_packs[entry.row()]; - if (!hasActiveInfoJob()) + if (!hasActiveInfoJob()) { m_current_info_job.clear(); + } if (!pack->versionsLoaded) { auto args{ createVersionsArguments(entry) }; @@ -203,20 +220,24 @@ void ResourceModel::loadEntry(const QModelIndex& entry) auto addonId = pack->addonId; // Use default if no callbacks are set - if (!callbacks.on_succeed) + if (!callbacks.on_succeed) { callbacks.on_succeed = [this, entry, addonId](auto& doc) { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } versionRequestSucceeded(doc, addonId, entry); }; - if (!callbacks.on_fail) - callbacks.on_fail = [](QString reason, int) { + } + if (!callbacks.on_fail) { + callbacks.on_fail = [](const QString& reason, int) { QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project versions: %1").arg(reason)); }; + } - if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) + if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) { runInfoJob(job); + } } if (!pack->extraDataLoaded) { @@ -224,41 +245,45 @@ void ResourceModel::loadEntry(const QModelIndex& entry) ResourceAPI::Callback callbacks{}; callbacks.on_succeed = [this, entry](auto& newpack) { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } infoRequestSucceeded(newpack, entry); }; - callbacks.on_fail = [this](QString reason, int) { - if (!s_running_models.constFind(this).value()) + callbacks.on_fail = [this](const QString& reason, int) { + if (!s_running_models.constFind(this).value()) { return; + } QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); }; callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) + if (!s_running_models.constFind(this).value()) { return; + } qCritical() << tr("The request was aborted for an unknown reason"); }; - if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) + if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) { runInfoJob(job); + } } } void ResourceModel::refresh() { - bool reset_requested = false; + bool resetRequested = false; if (hasActiveInfoJob()) { m_current_info_job.abort(); - reset_requested = true; + resetRequested = true; } if (hasActiveSearchJob()) { m_current_search_job->abort(); - reset_requested = true; + resetRequested = true; } - if (reset_requested) { + if (resetRequested) { m_search_state = SearchState::ResetRequested; return; } @@ -284,13 +309,15 @@ void ResourceModel::runSearchJob(Task::Ptr ptr) } void ResourceModel::runInfoJob(Task::Ptr ptr) { - if (!m_current_info_job.isRunning()) + if (!m_current_info_job.isRunning()) { m_current_info_job.clear(); + } - m_current_info_job.addTask(ptr); + m_current_info_job.addTask(std::move(ptr)); - if (!m_current_info_job.isRunning()) + if (!m_current_info_job.isRunning()) { m_current_info_job.run(); + } } std::optional ResourceModel::getCurrentSortingMethodByIndex() const @@ -298,11 +325,12 @@ std::optional ResourceModel::getCurrentSortingMethod std::optional sort{}; { // Find sorting method by ID - auto sorting_methods = getSortingMethods(); - auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(), - [this](auto const& e) { return m_current_sort_index == e.index; }); - if (method != sorting_methods.constEnd()) + auto sortingMethods = getSortingMethods(); + auto method = std::find_if(sortingMethods.constBegin(), sortingMethods.constEnd(), + [this](const auto& e) { return m_current_sort_index == e.index; }); + if (method != sortingMethods.constEnd()) { sort = *method; + } } return sort; @@ -311,43 +339,47 @@ std::optional ResourceModel::getCurrentSortingMethod std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) { QPixmap pixmap; - if (QPixmapCache::find(url.toString(), &pixmap)) + if (QPixmapCache::find(url.toString(), &pixmap)) { return { pixmap }; + } if (!m_current_icon_job) { m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); m_current_icon_job->setAskRetry(false); } - if (m_currently_running_icon_actions.contains(url)) + if (m_currently_running_icon_actions.contains(url)) { return {}; - if (m_failed_icon_actions.contains(url)) + } + if (m_failed_icon_actions.contains(url)) { return {}; + } - auto cache_entry = APPLICATION->metacache()->resolveEntry( + auto cacheEntry = APPLICATION->metacache()->resolveEntry( metaEntryBase(), QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); - auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry); + auto iconFetchAction = Net::ApiDownload::makeCached(url, cacheEntry); - auto full_file_path = cache_entry->getFullPath(); - connect(icon_fetch_action.get(), &Task::succeeded, this, [this, url, full_file_path, index] { - auto icon = QIcon(full_file_path); + auto fullFilePath = cacheEntry->getFullPath(); + connect(iconFetchAction.get(), &Task::succeeded, this, [this, url, fullFilePath, index] { + auto icon = QIcon(fullFilePath); QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); m_currently_running_icon_actions.remove(url); emit dataChanged(index, index, { Qt::DecorationRole }); }); - connect(icon_fetch_action.get(), &Task::failed, this, [this, url] { + connect(iconFetchAction.get(), &Task::failed, this, [this, url] { m_currently_running_icon_actions.remove(url); m_failed_icon_actions.insert(url); }); m_currently_running_icon_actions.insert(url); - m_current_icon_job->addNetAction(icon_fetch_action); - if (!m_current_icon_job->isRunning()) + m_current_icon_job->addNetAction(iconFetchAction); + if (!m_current_icon_job->isRunning()) { QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start); + } return {}; } @@ -359,11 +391,11 @@ void ResourceModel::searchRequestSucceeded(QList& QList filteredNewList; for (auto pack : newList) { ModPlatform::IndexedPack::Ptr p; - if (auto sel = std::find_if(m_selected.begin(), m_selected.end(), - [&pack](const DownloadTaskPtr i) { - const auto ipack = i->getPack(); - return ipack->provider == pack->provider && ipack->addonId == pack->addonId; - }); + if (auto sel = std::ranges::find_if(m_selected, + [&pack](const DownloadTaskPtr& i) { + const auto ipack = i->getPack(); + return ipack->provider == pack->provider && ipack->addonId == pack->addonId; + }); sel != m_selected.end()) { p = sel->get()->getPack(); } else { @@ -382,8 +414,9 @@ void ResourceModel::searchRequestSucceeded(QList& } // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (filteredNewList.size() == 0) + if (filteredNewList.size() == 0) { return; + } beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + filteredNewList.size() - 1); m_packs.append(filteredNewList); @@ -399,13 +432,16 @@ void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr p endInsertRows(); } -void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code) +void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int networkErrorCode) { - switch (network_error_code) { + switch (networkErrorCode) { default: // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); break; + case 404: + // 404 Not Found, some APIs return this when nothing is found, no need to bother the user + break; case 409: // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), @@ -413,13 +449,21 @@ void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int net break; } - m_search_state = SearchState::Finished; + if (m_search_state == SearchState::ResetRequested) { + clearData(); + + m_next_search_offset = 0; + search(); + } else { + m_search_state = SearchState::Finished; + } } void ResourceModel::searchRequestAborted() { - if (m_search_state != SearchState::ResetRequested) + if (m_search_state != SearchState::ResetRequested) { qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!"; + } // Retry fetching clearData(); @@ -430,19 +474,20 @@ void ResourceModel::searchRequestAborted() void ResourceModel::versionRequestSucceeded(QVector& doc, QVariant pack, const QModelIndex& index) { - auto current_pack = data(index, Qt::UserRole).value(); + auto currentPack = data(index, Qt::UserRole).value(); // Check if the index is still valid for this resource or not - if (pack != current_pack->addonId) + if (pack != currentPack->addonId) { return; + } - current_pack->versions = doc; - current_pack->versionsLoaded = true; + currentPack->versions = doc; + currentPack->versionsLoaded = true; // Cache info :^) - QVariant new_pack; - new_pack.setValue(current_pack); - if (!setData(index, new_pack, Qt::UserRole)) { + QVariant newPack; + newPack.setValue(currentPack); + if (!setData(index, newPack, Qt::UserRole)) { qWarning() << "Failed to cache resource versions!"; return; } @@ -452,16 +497,17 @@ void ResourceModel::versionRequestSucceeded(QVector void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, const QModelIndex& index) { - auto current_pack = data(index, Qt::UserRole).value(); + auto currentPack = data(index, Qt::UserRole).value(); // Check if the index is still valid for this resource or not - if (pack->addonId != current_pack->addonId) + if (pack->addonId != currentPack->addonId) { return; + } // Cache info :^) - QVariant new_pack; - new_pack.setValue(pack); - if (!setData(index, new_pack, Qt::UserRole)) { + QVariant newPack; + newPack.setValue(pack); + if (!setData(index, newPack, Qt::UserRole)) { qWarning() << "Failed to cache resource info!"; return; } @@ -471,16 +517,17 @@ void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, con void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - const std::shared_ptr packs, - bool is_indexed) + ResourceFolderModel* packs, + bool isIndexed, + QString downloadReason) { version.is_currently_selected = true; - m_selected.append(makeShared(pack, version, packs, is_indexed)); + m_selected.append(makeShared(std::move(pack), version, packs, isIndexed, std::move(downloadReason))); } void ResourceModel::removePack(const QString& rem) { - auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); }; + auto pred = [&rem](const DownloadTaskPtr& i) { return rem == i->getName(); }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_selected.removeIf(pred); #else @@ -492,15 +539,16 @@ void ResourceModel::removePack(const QString& rem) ++it; } #endif - auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; }); + auto pack = std::ranges::find_if(m_packs, [&rem](const ModPlatform::IndexedPack::Ptr& i) { return rem == i->name; }); if (pack == m_packs.end()) { // ignore it if is not in the current search return; } if (!pack->get()->versionsLoaded) { return; } - for (auto& ver : pack->get()->versions) + for (auto& ver : pack->get()->versions) { ver.is_currently_selected = false; + } } bool ResourceModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 8a0b7e06a..9124c0e66 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -36,16 +36,16 @@ class ResourceModel : public QAbstractListModel { ResourceModel(ResourceAPI* api); ~ResourceModel() override; - auto data(const QModelIndex&, int role) const -> QVariant override; + auto data(const QModelIndex& /*index*/, int role) const -> QVariant override; auto roleNames() const -> QHash override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; virtual auto debugName() const -> QString; virtual auto metaEntryBase() const -> QString = 0; - inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast(m_packs.size()); } - inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } - inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } + int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast(m_packs.size()); } + int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } + auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } @@ -66,7 +66,7 @@ class ResourceModel : public QAbstractListModel { public slots: void fetchMore(const QModelIndex& parent) override; - inline bool canFetchMore(const QModelIndex& parent) const override + bool canFetchMore(const QModelIndex& parent) const override { return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; } @@ -93,8 +93,9 @@ class ResourceModel : public QAbstractListModel { void addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - std::shared_ptr packs, - bool is_indexed = false); + ResourceFolderModel* packs, + bool isIndexed = false, + QString downloadReason = "standalone"); void removePack(const QString& rem); QList selectedPacks() { return m_selected; } diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index 53a40faa2..ac5121f2a 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -5,14 +5,15 @@ #include "ResourcePackModel.h" #include +#include namespace ResourceDownload { -ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, +ResourcePackResourceModel::ResourcePackResourceModel(const BaseInstance& base_inst, ResourceAPI* api, - QString debugName, + const QString& debugName, QString metaEntryBase) - : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(std::move(metaEntryBase)) {} /******** Make data requests ********/ @@ -20,19 +21,29 @@ ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_in ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::ResourcePack, m_next_search_offset, m_search_term, sort }; + return { + .type = ModPlatform::ResourceType::ResourcePack, + .offset = m_next_search_offset, + .search = m_search_term, + .sorting = sort, + .loaders = {}, + .versions = {}, + .side = {}, + .categoryIds = {}, + .openSource = {}, + }; } ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; - return { pack, {}, {}, ModPlatform::ResourceType::ResourcePack }; + return { .pack = pack, .mcVersions = {}, .loaders = {}, .resourceType = ModPlatform::ResourceType::ResourcePack }; } ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; - return { pack }; + return { .pack = pack }; } void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort) diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.h b/launcher/ui/pages/modplatform/ResourcePackModel.h index d664ccb05..92e3c4d37 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.h +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -8,8 +8,6 @@ #include "BaseInstance.h" -#include "modplatform/ModIndex.h" - #include "ui/pages/modplatform/ResourceModel.h" class Version; @@ -20,7 +18,7 @@ class ResourcePackResourceModel : public ResourceModel { Q_OBJECT public: - ResourcePackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); + ResourcePackResourceModel(const BaseInstance&, ResourceAPI*, const QString& debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 061e96491..8baa0bbec 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -39,12 +39,14 @@ #include "ResourcePage.h" #include "modplatform/ModIndex.h" -#include "ui/dialogs/CustomMessageBox.h" #include "ui_ResourcePage.h" #include #include #include +#include +#include +#include #include "Markdown.h" @@ -55,14 +57,15 @@ namespace ResourceDownload { -ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) - : QWidget(parent), m_baseInstance(base_instance), m_ui(new Ui::ResourcePage), m_parentDialog(parent), m_fetchProgress(this, false) +ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& baseInstance) + : QWidget(parent), m_baseInstance(baseInstance), m_ui(new Ui::ResourcePage), m_parentDialog(parent), m_fetchProgress(this, false) { m_ui->setupUi(this); m_ui->searchEdit->installEventFilter(this); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_searchTimer.setTimerType(Qt::TimerType::CoarseTimer); @@ -78,7 +81,7 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); - auto delegate = new ProjectItemDelegate(this); + auto* delegate = new ProjectItemDelegate(this); m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); m_ui->packView->viewport()->installEventFilter(this); @@ -92,8 +95,7 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in ResourcePage::~ResourcePage() { delete m_ui; - if (m_model) - delete m_model; + delete m_model; } void ResourcePage::retranslate() @@ -113,10 +115,19 @@ void ResourcePage::openedImpl() m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); updateSelectionButton(); - triggerSearch(); + if (!m_suppressInitialSearch) { + triggerSearch(); + } else { + m_suppressInitialSearch = false; + } m_ui->searchEdit->setFocus(); } +void ResourcePage::setSuppressInitialSearch(bool suppress) +{ + m_suppressInitialSearch = suppress; +} + auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool { if (event->type() == QEvent::KeyPress) { @@ -126,12 +137,13 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool triggerSearch(); keyEvent->accept(); return true; - } else { - if (m_searchTimer.isActive()) - m_searchTimer.stop(); - - m_searchTimer.start(350); } + if (m_searchTimer.isActive()) { + m_searchTimer.stop(); + } + + m_searchTimer.start(350); + } else if (watched == m_ui->packView) { // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { @@ -157,7 +169,7 @@ QString ResourcePage::getSearchTerm() const return m_ui->searchEdit->text(); } -void ResourcePage::setSearchTerm(QString term) +void ResourcePage::setSearchTerm(const QString& term) { m_ui->searchEdit->setText(term); } @@ -167,10 +179,11 @@ void ResourcePage::addSortings() Q_ASSERT(m_model); auto sorts = m_model->getSortingMethods(); - std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; }); + std::ranges::sort(sorts, [](const auto& l, const auto& r) { return l.index < r.index; }); - for (auto&& sorting : sorts) + for (auto&& sorting : sorts) { m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index)); + } } bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack) @@ -187,24 +200,26 @@ ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const void ResourcePage::updateUi(const QModelIndex& index) { - if (index != m_ui->packView->currentIndex()) + if (index != m_ui->packView->currentIndex()) { return; + } - auto current_pack = getCurrentPack(); - if (!current_pack) { + auto currentPack = getCurrentPack(); + if (!currentPack) { m_ui->packDescription->setHtml({}); m_ui->packDescription->flush(); return; } QString text = ""; - QString name = current_pack->name; + QString name = currentPack->name; - if (current_pack->websiteUrl.isEmpty()) + if (currentPack->websiteUrl.isEmpty()) { text = name; - else - text = "websiteUrl + "\">" + name + ""; + } else { + text = "websiteUrl + "\">" + name + ""; + } - if (!current_pack->authors.empty()) { + if (!currentPack->authors.empty()) { auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { if (author.url.isEmpty()) { return author.name; @@ -212,49 +227,53 @@ void ResourcePage::updateUi(const QModelIndex& index) return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; - for (auto& author : current_pack->authors) { + for (auto& author : currentPack->authors) { authorStrs.push_back(authorToStr(author)); } text += "
" + tr(" by ") + authorStrs.join(", "); } - if (current_pack->extraDataLoaded) { - if (current_pack->extraData.status == "archived") { + if (currentPack->extraDataLoaded) { + if (currentPack->extraData.status == "archived") { text += "

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

" + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; QStringList donates; - for (auto& donate : current_pack->extraData.donate) { + for (auto& donate : currentPack->extraData.donate) { donates.append(donateToStr(donate)); } text += donates.join(", "); } - if (!current_pack->extraData.issuesUrl.isEmpty() || !current_pack->extraData.sourceUrl.isEmpty() || - !current_pack->extraData.wikiUrl.isEmpty() || !current_pack->extraData.discordUrl.isEmpty()) { + if (!currentPack->extraData.issuesUrl.isEmpty() || !currentPack->extraData.sourceUrl.isEmpty() || + !currentPack->extraData.wikiUrl.isEmpty() || !currentPack->extraData.discordUrl.isEmpty()) { text += "

" + tr("External links:") + "
"; } - if (!current_pack->extraData.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current_pack->extraData.issuesUrl) + "
"; - if (!current_pack->extraData.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current_pack->extraData.wikiUrl) + "
"; - if (!current_pack->extraData.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current_pack->extraData.sourceUrl) + "
"; - if (!current_pack->extraData.discordUrl.isEmpty()) - text += "- " + tr("Discord: %1").arg(current_pack->extraData.discordUrl) + "
"; + if (!currentPack->extraData.issuesUrl.isEmpty()) { + text += "- " + tr("Issues: %1").arg(currentPack->extraData.issuesUrl) + "
"; + } + if (!currentPack->extraData.wikiUrl.isEmpty()) { + text += "- " + tr("Wiki: %1").arg(currentPack->extraData.wikiUrl) + "
"; + } + if (!currentPack->extraData.sourceUrl.isEmpty()) { + text += "- " + tr("Source code: %1").arg(currentPack->extraData.sourceUrl) + "
"; + } + if (!currentPack->extraData.discordUrl.isEmpty()) { + text += "- " + tr("Discord: %1").arg(currentPack->extraData.discordUrl) + "
"; + } } text += "
"; m_ui->packDescription->setHtml(StringUtils::htmlListPatch( - text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)))); + text + (currentPack->extraData.body.isEmpty() ? currentPack->description : markdownToHTML(currentPack->extraData.body)))); m_ui->packDescription->flush(); } @@ -266,14 +285,15 @@ void ResourcePage::updateSelectionButton() } m_ui->resourceSelectionButton->setEnabled(true); - if (auto current_pack = getCurrentPack(); current_pack) { - if (current_pack->versionsLoaded && current_pack->versions.empty()) { + if (auto currentPack = getCurrentPack(); currentPack) { + if (currentPack->versionsLoaded && currentPack->versions.empty()) { m_ui->resourceSelectionButton->setEnabled(false); qWarning() << tr("No version available for the selected pack"); - } else if (!current_pack->isVersionSelected(m_selectedVersionIndex)) + } else if (!currentPack->isVersionSelected(m_selectedVersionIndex)) { m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); - else + } else { m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); + } } else { qWarning() << "Tried to update the selected button but there is not a pack selected"; } @@ -282,19 +302,20 @@ void ResourcePage::updateSelectionButton() void ResourcePage::versionListUpdated(const QModelIndex& index) { if (index == m_ui->packView->currentIndex()) { - auto current_pack = getCurrentPack(); + auto currentPack = getCurrentPack(); m_ui->versionSelectionBox->blockSignals(true); m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->blockSignals(false); - if (current_pack) { - auto installedVersion = m_model->getInstalledPackVersion(current_pack); + if (currentPack) { + auto installedVersion = m_model->getInstalledPackVersion(currentPack); - for (int i = 0; i < current_pack->versions.size(); i++) { - auto& version = current_pack->versions[i]; - if (!m_model->checkVersionFilters(version)) + for (int i = 0; i < currentPack->versions.size(); i++) { + auto& version = currentPack->versions[i]; + if (!m_model->checkVersionFilters(version)) { continue; + } auto versionText = version.version; if (version.version_type.isValid()) { @@ -315,8 +336,9 @@ void ResourcePage::versionListUpdated(const QModelIndex& index) if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); onResourceToggle(index); - } else + } else { updateSelectionButton(); + } } else if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); onResourceToggle(index); @@ -329,27 +351,30 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI return; } - auto current_pack = getCurrentPack(); + auto currentPack = getCurrentPack(); - bool request_load = false; - if (!current_pack || !current_pack->versionsLoaded) { + bool requestLoad = false; + if (!currentPack || !currentPack->versionsLoaded) { m_ui->resourceSelectionButton->setText(tr("Loading versions...")); m_ui->resourceSelectionButton->setEnabled(false); - request_load = true; + requestLoad = true; } else { versionListUpdated(curr); } - if (current_pack && !current_pack->extraDataLoaded) - request_load = true; + if (currentPack && !currentPack->extraDataLoaded) { + requestLoad = true; + } // we are already requesting this - if (m_enableQueue.contains(curr.row())) - request_load = false; + if (m_enableQueue.contains(curr.row())) { + requestLoad = false; + } - if (request_load) + if (requestLoad) { m_model->loadEntry(curr); + } updateUi(curr); } @@ -362,20 +387,21 @@ void ResourcePage::onVersionSelectionChanged(int index) void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version) { - m_parentDialog->addResource(pack, version); + m_parentDialog->addResource(std::move(pack), version); } -void ResourcePage::removeResourceFromDialog(const QString& pack_name) +void ResourcePage::removeResourceFromDialog(const QString& packName) { - m_parentDialog->removeResource(pack_name); + m_parentDialog->removeResource(packName); } void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver, - const std::shared_ptr base_model) + ResourceFolderModel* baseModel, + QString downloadReason) { - bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_model->addPack(pack, ver, base_model, is_indexed); + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(std::move(pack), ver, baseModel, isIndexed, std::move(downloadReason)); } void ResourcePage::modelReset() @@ -390,22 +416,25 @@ void ResourcePage::removeResourceFromPage(const QString& name) void ResourcePage::onResourceSelected() { - if (m_selectedVersionIndex < 0) + if (m_selectedVersionIndex < 0) { return; + } - auto current_pack = getCurrentPack(); - if (!current_pack || !current_pack->versionsLoaded || current_pack->versions.size() < m_selectedVersionIndex) + auto currentPack = getCurrentPack(); + if (!currentPack || !currentPack->versionsLoaded || currentPack->versions.size() < m_selectedVersionIndex) { return; + } - auto& version = current_pack->versions[m_selectedVersionIndex]; + auto& version = currentPack->versions[m_selectedVersionIndex]; Q_ASSERT(!version.downloadUrl.isNull()); - if (version.is_currently_selected) - removeResourceFromDialog(current_pack->name); - else - addResourceToDialog(current_pack, version); + if (version.is_currently_selected) { + removeResourceFromDialog(currentPack->name); + } else { + addResourceToDialog(currentPack, version); + } // Save the modified pack (and prevent warning in release build) - [[maybe_unused]] bool set = setCurrentPack(current_pack); + [[maybe_unused]] bool set = setCurrentPack(currentPack); Q_ASSERT(set); updateSelectionButton(); @@ -420,26 +449,28 @@ void ResourcePage::onResourceToggle(const QModelIndex& index) auto pack = m_model->data(index, Qt::UserRole).value(); if (pack->versionsLoaded) { - if (pack->isAnyVersionSelected()) + if (pack->isAnyVersionSelected()) { removeResourceFromDialog(pack->name); - else { + } else { auto version = std::find_if(pack->versions.begin(), pack->versions.end(), [this](const ModPlatform::IndexedVersion& version) { return m_model->checkVersionFilters(version); }); if (version == pack->versions.end()) { - auto errorMessage = new QMessageBox( + auto* errorMessage = new QMessageBox( QMessageBox::Warning, tr("No versions available"), tr("No versions for '%1' are available.\nThe author likely blocked third-party launchers.").arg(pack->name), QMessageBox::Ok, this); errorMessage->open(); - } else + } else { addResourceToDialog(pack, *version); + } } - if (isSelected) + if (isSelected) { updateSelectionButton(); + } // force update QVariant variant; @@ -451,8 +482,9 @@ void ResourcePage::onResourceToggle(const QModelIndex& index) // we can't be sure that this hasn't already been requested... // but this does the job well enough and there's not much point preventing edgecases - if (!isSelected) + if (!isSelected) { m_model->loadEntry(index); + } } } @@ -483,13 +515,13 @@ void ResourcePage::openUrl(const QUrl& url) const QString slug = match.captured(1); // ensure the user isn't opening the same mod - if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { + if (auto currentPack = getCurrentPack(); currentPack && slug != currentPack->slug) { m_parentDialog->selectPage(page); - auto newPage = m_parentDialog->selectedPage(); + auto* newPage = m_parentDialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; - auto model = newPage->m_model; + auto* model = newPage->m_model; QListView* view = newPage->m_ui->packView; auto jump = [url, slug, model, view] { @@ -510,10 +542,11 @@ void ResourcePage::openUrl(const QUrl& url) searchEdit->setText(slug); newPage->triggerSearch(); - if (model->hasActiveSearchJob()) + if (model->hasActiveSearchJob()) { connect(model->activeSearchJob().get(), &Task::finished, jump); - else + } else { jump(); + } return; } @@ -523,7 +556,7 @@ void ResourcePage::openUrl(const QUrl& url) QDesktopServices::openUrl(url); } -void ResourcePage::openProject(QVariant projectID) +void ResourcePage::openProject(const QVariant& projectID) { m_ui->sortByBox->hide(); m_ui->searchEdit->hide(); @@ -532,16 +565,16 @@ void ResourcePage::openProject(QVariant projectID) m_ui->resourceSelectionButton->hide(); m_doNotJumpToMod = true; - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + auto* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - auto okBtn = buttonBox->button(QDialogButtonBox::Ok); + auto* okBtn = buttonBox->button(QDialogButtonBox::Ok); okBtn->setDefault(true); okBtn->setAutoDefault(true); okBtn->setText(tr("Reinstall")); okBtn->setShortcut(tr("Ctrl+Return")); okBtn->setEnabled(false); - auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); + auto* cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); cancelBtn->setDefault(false); cancelBtn->setAutoDefault(false); cancelBtn->setText(tr("Cancel")); @@ -558,9 +591,8 @@ void ResourcePage::openProject(QVariant projectID) [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); auto jump = [this] { - for (int row = 0; row < m_model->rowCount({}); row++) { - const QModelIndex index = m_model->index(row); - m_ui->packView->setCurrentIndex(index); + if (m_model->rowCount({}) > 0) { + m_ui->packView->setCurrentIndex(m_model->index(0)); return; } m_ui->packDescription->setText(tr("The resource was not found")); @@ -569,9 +601,10 @@ void ResourcePage::openProject(QVariant projectID) m_ui->searchEdit->setText("#" + projectID.toString()); triggerSearch(); - if (m_model->hasActiveSearchJob()) + if (m_model->hasActiveSearchJob()) { connect(m_model->activeSearchJob().get(), &Task::finished, jump); - else + } else { jump(); + } } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 8f4d2c496..03895dcd4 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -9,7 +9,6 @@ #include "ResourceDownloadTask.h" #include "modplatform/ModIndex.h" -#include "modplatform/ResourceAPI.h" #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ResourceModel.h" @@ -44,9 +43,9 @@ class ResourcePage : public QWidget, public BasePage { virtual auto debugName() const -> QString = 0; //: The plural version of 'resource' - virtual inline QString resourcesString() const { return tr("resources"); } + virtual QString resourcesString() const { return tr("resources"); } //: The singular version of 'resources' - virtual inline QString resourceString() const { return tr("resource"); } + virtual QString resourceString() const { return tr("resource"); } /* Features this resource's page supports */ virtual bool supportsFiltering() const = 0; @@ -58,7 +57,7 @@ class ResourcePage : public QWidget, public BasePage { /** Get the current term in the search bar. */ auto getSearchTerm() const -> QString; /** Programatically set the term in the search bar. */ - void setSearchTerm(QString); + void setSearchTerm(const QString&); bool setCurrentPack(ModPlatform::IndexedPack::Ptr); auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; @@ -76,21 +75,26 @@ class ResourcePage : public QWidget, public BasePage { virtual void versionListUpdated(const QModelIndex& index); void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); - void removeResourceFromDialog(const QString& pack_name); + void removeResourceFromDialog(const QString& packName); virtual void removeResourceFromPage(const QString& name); - virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr); + virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, + ModPlatform::IndexedVersion&, + ResourceFolderModel*, + QString downloadReason = "standalone"); virtual void modelReset(); QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } - virtual void openProject(QVariant projectID); + virtual void openProject(const QVariant& projectID); + + void setSuppressInitialSearch(bool suppress); protected slots: virtual void triggerSearch() = 0; - void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSelectionChanged(QModelIndex curr, QModelIndex prev); void onVersionSelectionChanged(int index); void onResourceSelected(); void onResourceToggle(const QModelIndex& index); @@ -118,6 +122,9 @@ class ResourcePage : public QWidget, public BasePage { bool m_doNotJumpToMod = false; QSet m_enableQueue; + + private: + bool m_suppressInitialSearch = false; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePage.ui b/launcher/ui/pages/modplatform/ResourcePage.ui index 491e7d9f0..a0eb40864 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.ui +++ b/launcher/ui/pages/modplatform/ResourcePage.ui @@ -47,6 +47,9 @@ 48 + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index 5349b69ab..e8a6bcaf5 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -5,11 +5,15 @@ #include "ShaderPackModel.h" #include +#include namespace ResourceDownload { -ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) - : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) +ShaderPackResourceModel::ShaderPackResourceModel(const BaseInstance& base_inst, + ResourceAPI* api, + const QString& debugName, + QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(std::move(metaEntryBase)) {} /******** Make data requests ********/ @@ -17,19 +21,29 @@ ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::ShaderPack, m_next_search_offset, m_search_term, sort }; + return { + .type = ModPlatform::ResourceType::ShaderPack, + .offset = m_next_search_offset, + .search = m_search_term, + .sorting = sort, + .loaders = {}, + .versions = {}, + .side = {}, + .categoryIds = {}, + .openSource = {}, + }; } ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; - return { pack, {}, {}, ModPlatform::ResourceType::ShaderPack }; + return { .pack = pack, .mcVersions = {}, .loaders = {}, .resourceType = ModPlatform::ResourceType::ShaderPack }; } ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; - return { pack }; + return { .pack = pack }; } void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.h b/launcher/ui/pages/modplatform/ShaderPackModel.h index 9856be93e..cadaf17ee 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.h +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -20,7 +20,7 @@ class ShaderPackResourceModel : public ResourceModel { Q_OBJECT public: - ShaderPackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); + ShaderPackResourceModel(const BaseInstance&, ResourceAPI*, const QString& debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index fda05b038..ec5fe7967 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -36,18 +36,18 @@ QMap ShaderPackResourcePage::urlHandlers() const { QMap map; map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth"); - map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), - "curseforge"); - map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern(R"((?:www\.)?curseforge\.com\/minecraft\/customization\/([^\/]+)\/?)"), "curseforge"); + map.insert(QRegularExpression::anchoredPattern(R"(minecraft\.curseforge\.com\/projects\/([^\/]+)\/?)"), "curseforge"); return map; } void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - const std::shared_ptr base_model) + ResourceFolderModel* baseModel, + QString downloadReason) { - bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_model->addPack(pack, version, base_model, is_indexed); + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(pack, version, baseModel, isIndexed, downloadReason); } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index 85d2b16e6..37e3cadef 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -23,7 +23,7 @@ class ShaderPackResourcePage : public ResourcePage { static T* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); - auto model = static_cast(page->getModel()); + auto* model = static_cast(page->getModel()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); @@ -33,17 +33,20 @@ class ShaderPackResourcePage : public ResourcePage { } //: The plural version of 'shader pack' - inline QString resourcesString() const override { return tr("shader packs"); } + QString resourcesString() const override { return tr("shader packs"); } //: The singular version of 'shader packs' - inline QString resourceString() const override { return tr("shader pack"); } + QString resourceString() const override { return tr("shader pack"); } bool supportsFiltering() const override { return false; }; - void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; + void addResourceToPage(ModPlatform::IndexedPack::Ptr /*unused*/, + ModPlatform::IndexedVersion& /*unused*/, + ResourceFolderModel* /*unused*/, + QString downloadReason = "standalone") override; QMap urlHandlers() const override; - inline auto helpPage() const -> QString override { return "shaderpack-platform"; } + auto helpPage() const -> QString override { return "shaderpack-platform"; } protected: ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index 7c1490671..32ec488ab 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -4,16 +4,22 @@ #include "TexturePackModel.h" +#include + #include "Application.h" #include "meta/Index.h" #include "meta/Version.h" -static std::list s_availableVersions = {}; +static std::vector s_availableVersions = {}; namespace ResourceDownload { -TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase) - : ResourcePackResourceModel(inst, api, debugName, metaEntryBase), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) +TexturePackResourceModel::TexturePackResourceModel(const BaseInstance& inst, + ResourceAPI* api, + const QString& debugName, + QString metaEntryBase) + : ResourcePackResourceModel(inst, api, debugName, std::move(metaEntryBase)) + , m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index bb7348b33..0e1e3f3fb 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -13,7 +13,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { Q_OBJECT public: - TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase); + TexturePackResourceModel(const BaseInstance& inst, ResourceAPI* api, const QString& debugName, QString metaEntryBase); inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 798da888b..91668fb84 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -99,23 +99,26 @@ void ListModel::request() auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(url)); + netJob->addNetAction(action); jobPtr = netJob; jobPtr->start(); - connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + connect(netJob.get(), &NetJob::succeeded, this, [this, response] { requestFinished(response); }); connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } -void ListModel::requestFinished() +void ListModel::requestFinished(QByteArray* responsePtr) { + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *response; + qWarning() << response; return; } @@ -130,7 +133,7 @@ void ListModel::requestFinished() try { ATLauncher::loadIndexedPack(pack, packObj); } catch (const JSONValidationError& e) { - qDebug() << QString::fromUtf8(*response); + qDebug() << QString::fromUtf8(response); qWarning() << "Error while reading pack manifest from ATLauncher:" << e.cause(); return; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h index bcadd7c91..51c5c782d 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -43,7 +43,7 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); private slots: - void requestFinished(); + void requestFinished(QByteArray* responsePtr); void requestFailed(QString reason); void logoFailed(QString logo); @@ -61,7 +61,6 @@ class ListModel : public QAbstractListModel { QMap waitingCallbacks; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); }; } // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 3e072d441..421060e85 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -159,23 +159,26 @@ void AtlOptionalModListModel::useShareCode(const QString& code) { m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); - m_jobPtr->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), m_response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(url)); + m_jobPtr->addNetAction(action); - connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); + connect(m_jobPtr.get(), &NetJob::succeeded, this, [this, response] { shareCodeSuccess(response); }); connect(m_jobPtr.get(), &NetJob::failed, this, &AtlOptionalModListModel::shareCodeFailure); m_jobPtr->start(); } -void AtlOptionalModListModel::shareCodeSuccess() +void AtlOptionalModListModel::shareCodeSuccess(QByteArray* responsePtr) { + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray responseData = *std::move(responsePtr); m_jobPtr.reset(); QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*m_response, &parse_error); + auto doc = QJsonDocument::fromJson(responseData, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *m_response; + qWarning() << responseData; return; } auto obj = doc.object(); @@ -184,7 +187,7 @@ void AtlOptionalModListModel::shareCodeSuccess() try { ATLauncher::loadShareCodeResponse(response, obj); } catch (const JSONValidationError& e) { - qDebug() << QString::fromUtf8(*m_response); + qDebug() << QString::fromUtf8(responseData); qWarning() << "Error while reading response from ATLauncher:" << e.cause(); return; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index fa39e997c..8c36320e0 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -71,7 +71,7 @@ class AtlOptionalModListModel : public QAbstractListModel { void useShareCode(const QString& code); public slots: - void shareCodeSuccess(); + void shareCodeSuccess(QByteArray* responsePtr); void shareCodeFailure(const QString& reason); void selectRecommended(); @@ -83,7 +83,6 @@ class AtlOptionalModListModel : public QAbstractListModel { private: NetJob::Ptr m_jobPtr; - std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; QList m_mods; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui index d9496142a..717d0cca0 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -49,7 +49,11 @@
- + + + QAbstractItemView::ScrollPerPixel + + diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index ad49d940e..14267bb1c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -61,6 +61,7 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui->packView->setIndentation(0); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); for (int i = 0; i < filterModel->getAvailableSortings().size(); i++) { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 0b1411b96..3fc0e55a4 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -52,6 +52,9 @@ 48 + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 5d968d65a..861dd9f22 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -165,12 +165,20 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { static const FlameAPI api; - if (m_currentSearchTerm.startsWith("#")) { + + // activate search by id only for numerical values because all CurseForge ids are numerical + static const QRegularExpression s_projectIdExpr("^\\#[0-9]+$"); + if (m_searchState != ResetRequested && s_projectIdExpr.match(m_currentSearchTerm).hasMatch()) { auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_fail = [this](QString reason, int network_error_code) { + if (network_error_code == 404) { + m_searchState = ResetRequested; + } + searchRequestFailed(reason); + }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; @@ -178,7 +186,7 @@ void ListModel::performPaginatedSearch() }; auto project = std::make_shared(); project->addonId = projectId; - if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { + if (auto job = api.getProjectInfo({ project }, std::move(callbacks), false); job) { m_jobPtr = job; m_jobPtr->start(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 79faccbc1..336133819 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -62,6 +62,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) m_ui->packView->setModel(m_listModel); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); @@ -329,10 +330,10 @@ void FlamePage::createFilterWidget() connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); - auto response = std::make_shared(); - m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::Modpack); + auto [task, response] = FlameAPI::getCategories(ModPlatform::ResourceType::Modpack); + m_categoriesTask = task; connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = FlameAPI::loadModCategories(response); + auto categories = FlameAPI::loadModCategories(*response); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index cf882ef1c..5d72f7513 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -72,6 +72,9 @@ 48 + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 6ff435854..acdce29b6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -41,7 +41,7 @@ #include #include #include "modplatform/flame/FlameAPI.h" -#include "ui_ResourcePage.h" +#include "../ui_ResourcePage.h" #include "FlameResourceModels.h" #include "ui/dialogs/ResourceDownloadDialog.h" @@ -247,10 +247,10 @@ std::unique_ptr FlameModPage::createFilterWidget() void FlameModPage::prepareProviderCategories() { - auto response = std::make_shared(); - m_categoriesTask = FlameAPI::getModCategories(response); + auto [task, response] = FlameAPI::getModCategories(); + m_categoriesTask = task; connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = FlameAPI::loadModCategories(response); + auto categories = FlameAPI::loadModCategories(*response); m_filter_widget->setCategories(categories); }); m_categoriesTask->start(); diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp new file mode 100644 index 000000000..e33dda980 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * 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 "FtbFilterModel.h" + +#include + +#include "modplatform/ftb/FTBPackManifest.h" + +#include "StringUtils.h" + +namespace Ftb { + +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) +{ + m_currentSorting = Sorting::ByPlays; + m_sortings.insert(tr("Sort by Plays"), Sorting::ByPlays); + m_sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls); + m_sortings.insert(tr("Sort by Name"), Sorting::ByName); +} + +const QMap FilterModel::getAvailableSortings() +{ + return m_sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return m_sortings.key(m_currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + m_currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return m_currentSorting; +} + +void FilterModel::setSearchTerm(const QString& term) +{ + m_searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + if (m_searchTerm.isEmpty()) { + return true; + } + + auto index = sourceModel()->index(sourceRow, 0, sourceParent); + auto pack = sourceModel()->data(index, Qt::UserRole).value(); + return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); +} + +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + FTB::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + FTB::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if (m_currentSorting == ByPlays) { + return leftPack.plays < rightPack.plays; + } else if (m_currentSorting == ByInstalls) { + return leftPack.installs < rightPack.installs; + } else if (m_currentSorting == ByName) { + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} // namespace Ftb diff --git a/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h new file mode 100644 index 000000000..b9b958f05 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbFilterModel.h @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Ftb { + +class FilterModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPlays, + ByInstalls, + ByName, + }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(const QString& term); + + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; + + private: + QMap m_sortings; + Sorting m_currentSorting; + QString m_searchTerm{ "" }; +}; + +} // namespace Ftb diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp new file mode 100644 index 000000000..29d73a4a9 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.cpp @@ -0,0 +1,254 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * 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 "FtbListModel.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include + +namespace Ftb { + +ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} + +ListModel::~ListModel() {} + +int ListModel::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : 1; +} + +QVariant ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } + + FTB::Modpack pack = m_modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + return pack.synopsis; + } else if (role == Qt::DecorationRole) { + QIcon placeholder = QIcon::fromTheme("screenshot-placeholder"); + + auto iter = m_logoMap.find(pack.safeName); + if (iter != m_logoMap.end()) { + auto& logo = *iter; + if (!logo.result.isNull()) { + return logo.result; + } + return placeholder; + } + + for (auto art : pack.art) { + if (art.type == "square") { + ((ListModel*)this)->requestLogo(pack.safeName, art.url); + } + } + return placeholder; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo))->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ListModel::request() +{ + m_aborted = false; + + beginResetModel(); + m_modpacks.clear(); + endResetModel(); + + auto netJob = makeShared("Ftb::Request", APPLICATION->network()); + auto url = QString(BuildConfig.FTB_API_BASE_URL + "/modpack/all"); + auto [action, response] = Net::Download::makeByteArray(QUrl(url)); + netJob->addNetAction(action); + m_jobPtr = netJob; + m_jobPtr->start(); + + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, response] { requestFinished(response); }); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::abortRequest() +{ + m_aborted = m_jobPtr->abort(); + m_jobPtr.reset(); +} + +void ListModel::requestFinished(QByteArray* responsePtr) +{ + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by m_jobPtr.reset() + QByteArray response = std::move(*responsePtr); + m_jobPtr.reset(); + m_remainingPacks.clear(); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto packs = doc.object().value("packs").toArray(); + for (auto pack : packs) { + auto packId = pack.toInt(); + m_remainingPacks.append(packId); + } + + if (!m_remainingPacks.isEmpty()) { + m_currentPack = m_remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::requestFailed(QString) +{ + m_jobPtr.reset(); + m_remainingPacks.clear(); +} + +void ListModel::requestPack() +{ + auto netJob = makeShared("Ftb::Search", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.FTB_API_BASE_URL + "/modpack/%1").arg(m_currentPack); + auto [action, response] = Net::Download::makeByteArray(QUrl(searchUrl)); + netJob->addNetAction(action); + m_jobPtr = netJob; + m_jobPtr->start(); + + QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, response] { packRequestFinished(response); }); + QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::packRequestFailed); +} + +void ListModel::packRequestFinished(QByteArray* responsePtr) +{ + if (!m_jobPtr || m_aborted) + return; + + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); + + m_jobPtr.reset(); + m_remainingPacks.removeOne(m_currentPack); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + FTB::Modpack pack; + try { + FTB::loadModpack(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + return; + } + + // Since there is no guarantee that packs have a version, this will just + // ignore those "dud" packs. + if (pack.versions.empty()) { + qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions"; + } else { + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size()); + m_modpacks.append(pack); + endInsertRows(); + } + + if (!m_remainingPacks.isEmpty()) { + m_currentPack = m_remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::packRequestFailed(QString) +{ + m_jobPtr.reset(); + m_remainingPacks.removeOne(m_currentPack); +} + +void ListModel::logoLoaded(QString logo) +{ + auto& logoObj = m_logoMap[logo]; + logoObj.downloadJob.reset(); + logoObj.result = QIcon(logoObj.fullpath); + for (int i = 0; i < m_modpacks.size(); i++) { + if (m_modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_logoMap[logo].failed = true; + m_logoMap[logo].downloadJob.reset(); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if (m_logoMap.contains(logo)) { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo)); + + auto job = makeShared(QString("FTB Icon Download %1").arg(logo), APPLICATION->network()); + job->setAskRetry(false); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job.get(), &NetJob::finished, this, [this, logo, fullPath] { logoLoaded(logo); }); + + QObject::connect(job.get(), &NetJob::failed, this, [this, logo] { logoFailed(logo); }); + + auto& newLogoEntry = m_logoMap[logo]; + newLogoEntry.downloadJob = job; + newLogoEntry.fullpath = fullPath; + job->start(); +} + +} // namespace Ftb diff --git a/launcher/ui/pages/modplatform/ftb/FtbListModel.h b/launcher/ui/pages/modplatform/ftb/FtbListModel.h new file mode 100644 index 000000000..339693c7c --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbListModel.h @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include "modplatform/ftb/FTBPackManifest.h" +#include "net/NetJob.h" + +namespace Ftb { + +struct Logo { + QString fullpath; + NetJob::Ptr downloadJob; + QIcon result; + bool failed = false; +}; + +using LogoMap = QMap; +using LogoCallback = std::function; + +class ListModel : public QAbstractListModel { + Q_OBJECT + + public: + ListModel(QObject* parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + + void request(); + void abortRequest(); + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + + [[nodiscard]] bool isMakingRequest() const { return m_jobPtr.get(); } + [[nodiscard]] bool wasAborted() const { return m_aborted; } + + private slots: + void requestFinished(QByteArray* responsePtr); + void requestFailed(QString reason); + + void requestPack(); + void packRequestFinished(QByteArray* responsePtr); + void packRequestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo); + + private: + void requestLogo(QString file, QString url); + + private: + bool m_aborted = false; + + QList m_modpacks; + LogoMap m_logoMap; + + NetJob::Ptr m_jobPtr; + int m_currentPack; + QList m_remainingPacks; +}; + +} // namespace Ftb diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp new file mode 100644 index 000000000..b208f5c74 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Philip T + * + * 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 "FtbPage.h" +#include "ui_FtbPage.h" + +#include + +#include "modplatform/ftb/FTBPackInstallTask.h" +#include "ui/dialogs/NewInstanceDialog.h" + +#include "Markdown.h" + +FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), m_ui(new Ui::FtbPage), m_dialog(dialog) +{ + m_ui->setupUi(this); + + m_filterModel = new Ftb::FilterModel(this); + m_listModel = new Ftb::ListModel(this); + m_filterModel->setSourceModel(m_listModel); + m_ui->packView->setModel(m_filterModel); + m_ui->packView->setSortingEnabled(true); + m_ui->packView->header()->hide(); + m_ui->packView->setIndentation(0); + + m_ui->searchEdit->installEventFilter(this); + + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for (int i = 0; i < m_filterModel->getAvailableSortings().size(); i++) { + m_ui->sortByBox->addItem(m_filterModel->getAvailableSortings().keys().at(i)); + } + m_ui->sortByBox->setCurrentText(m_filterModel->translateCurrentSorting()); + + connect(m_ui->searchEdit, &QLineEdit::textChanged, this, &FtbPage::triggerSearch); + connect(m_ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged); + + m_ui->packDescription->setMetaEntry("FTBPacks"); +} + +FtbPage::~FtbPage() +{ + delete m_ui; +} + +bool FtbPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FtbPage::shouldDisplay() const +{ + return true; +} + +void FtbPage::retranslate() +{ + m_ui->retranslateUi(this); +} + +void FtbPage::openedImpl() +{ + if (!m_initialised || m_listModel->wasAborted()) { + m_listModel->request(); + m_initialised = true; + } + + suggestCurrent(); +} + +void FtbPage::closedImpl() +{ + if (m_listModel->isMakingRequest()) + m_listModel->abortRequest(); +} + +void FtbPage::suggestCurrent() +{ + if (!isOpened) { + return; + } + + if (m_selectedVersion.isEmpty()) { + m_dialog->setSuggestedPack(); + return; + } + + m_dialog->setSuggestedPack(m_selected.name, m_selectedVersion, new FTB::PackInstallTask(m_selected, m_selectedVersion, this)); + for (auto art : m_selected.art) { + if (art.type == "square") { + auto editedLogoName = "ftb_" + m_selected.safeName; + m_listModel->getLogo(m_selected.safeName, art.url, + [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); + } + } +} + +void FtbPage::triggerSearch() +{ + m_filterModel->setSearchTerm(m_ui->searchEdit->text()); +} + +void FtbPage::onSortingSelectionChanged(QString selected) +{ + auto toSet = m_filterModel->getAvailableSortings().value(selected); + m_filterModel->setSorting(toSet); +} + +void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex /*second*/) +{ + m_ui->versionSelectionBox->clear(); + + if (!first.isValid()) { + if (isOpened) { + m_dialog->setSuggestedPack(); + } + return; + } + + m_selected = m_filterModel->data(first, Qt::UserRole).value(); + + QString output = markdownToHTML(m_selected.description.toUtf8()); + m_ui->packDescription->setHtml(output); + + // reverse foreach, so that the newest versions are first + for (auto i = m_selected.versions.size(); i--;) { + m_ui->versionSelectionBox->addItem(m_selected.versions.at(i).name); + } + + suggestCurrent(); +} + +void FtbPage::onVersionSelectionChanged(QString selected) +{ + if (selected.isNull() || selected.isEmpty()) { + m_selectedVersion = ""; + return; + } + + m_selectedVersion = selected; + suggestCurrent(); +} + +QString FtbPage::getSerachTerm() const +{ + return m_ui->searchEdit->text(); +} + +void FtbPage::setSearchTerm(QString term) +{ + m_ui->searchEdit->setText(term); +} \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.h b/launcher/ui/pages/modplatform/ftb/FtbPage.h new file mode 100644 index 000000000..84e7740d4 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "FtbFilterModel.h" +#include "FtbListModel.h" + +#include + +#include "Application.h" +#include "tasks/Task.h" +#include "ui/pages/BasePage.h" +#include "ui/pages/modplatform/ModpackProviderBasePage.h" + +namespace Ui { +class FtbPage; +} + +class NewInstanceDialog; + +class FtbPage : public QWidget, public ModpackProviderBasePage { + Q_OBJECT + + public: + explicit FtbPage(NewInstanceDialog* dialog, QWidget* parent = 0); + virtual ~FtbPage(); + virtual QString displayName() const override { return "FTB"; } + virtual QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } + virtual QString id() const override { return "ftb"; } + virtual QString helpPage() const override { return "FTB-platform"; } + virtual bool shouldDisplay() const override; + void retranslate() override; + + void openedImpl() override; + void closedImpl() override; + + bool eventFilter(QObject* watched, QEvent* event) override; + + /** Programatically set the term in the search bar. */ + virtual void setSearchTerm(QString) override; + /** Get the current term in the search bar. */ + [[nodiscard]] virtual QString getSerachTerm() const override; + + private: + void suggestCurrent(); + + private slots: + void triggerSearch(); + + void onSortingSelectionChanged(QString selected); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString selected); + + private: + Ui::FtbPage* m_ui = nullptr; + NewInstanceDialog* m_dialog = nullptr; + Ftb::ListModel* m_listModel = nullptr; + Ftb::FilterModel* m_filterModel = nullptr; + + FTB::Modpack m_selected; + QString m_selectedVersion; + + bool m_initialised{ false }; +}; diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.ui b/launcher/ui/pages/modplatform/ftb/FtbPage.ui new file mode 100644 index 000000000..e7fe6f482 --- /dev/null +++ b/launcher/ui/pages/modplatform/ftb/FtbPage.ui @@ -0,0 +1,96 @@ + + + FtbPage + + + + 0 + 0 + 875 + 745 + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + Search and filter... + + + true + + + + + + + + + true + + + + 48 + 48 + + + + QAbstractItemView::ScrollPerPixel + + + + + + + true + + + true + + + + + + + + + Note: Many recent FTB modpacks are also available from CurseForge! + + + + + + + + ProjectDescriptionPage + QTextBrowser +
ui/widgets/ProjectDescriptionPage.h
+
+
+ + searchEdit + versionSelectionBox + + + +
diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index bb9990650..aa9b5aee2 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -61,6 +61,9 @@
+ + QAbstractItemView::ScrollPerPixel + 16777215 diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index 0e352f419..5c9c2fd72 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -23,6 +23,7 @@ #include #include #include "Application.h" +#include "settings/SettingsObject.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index eb95b291c..ab2bc6a67 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -35,6 +35,7 @@ #include "ListModel.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "net/ApiDownload.h" #include "net/HttpMetaCache.h" #include "net/NetJob.h" @@ -191,6 +192,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const // bugged pack, currently only indicates bugged xml return QColor(244, 229, 66); } + return {}; } case Qt::DisplayRole: return pack.name; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 1576f52fe..be4d3161d 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -107,6 +107,7 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog } ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index 544ad77d3..d3d696b80 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -46,6 +46,9 @@ true + + QAbstractItemView::ScrollPerPixel + @@ -80,6 +83,9 @@ true + + QAbstractItemView::ScrollPerPixel + @@ -100,6 +106,9 @@ true + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 05cd2970c..03518c3af 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -135,20 +135,26 @@ void ModpackListModel::performPaginatedSearch() return; static const ModrinthAPI api; - if (m_currentSearchTerm.startsWith("#")) { + // Modrinth ids are not limited to numbers and can be any length + if (m_searchState != ResetRequested && m_currentSearchTerm.startsWith("#")) { auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_fail = [this](QString reason, int network_error_code) { + if (network_error_code == 404) { + m_searchState = ResetRequested; + } + searchRequestFailed(reason, network_error_code); + }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; - searchRequestFailed("Aborted"); + searchRequestFailed("Aborted", 0); }; auto project = std::make_shared(); project->addonId = projectId; - if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { + if (auto job = api.getProjectInfo({ project }, std::move(callbacks), false); job) { m_jobPtr = job; m_jobPtr->start(); } @@ -161,10 +167,10 @@ void ModpackListModel::performPaginatedSearch() ResourceAPI::Callback> callbacks{}; callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; - callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_fail = [this](QString reason, int network_error_code) { searchRequestFailed(reason, network_error_code); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; - searchRequestFailed("Aborted"); + searchRequestFailed("Aborted", 0); }; auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, @@ -316,13 +322,12 @@ void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Pt endInsertRows(); } -void ModpackListModel::searchRequestFailed(QString) +void ModpackListModel::searchRequestFailed(QString reason, int network_error_code) { - auto failed_action = dynamic_cast(m_jobPtr.get())->getFailedActions().at(0); - if (failed_action->replyStatusCode() == -1) { - // Network error + if (network_error_code == -1) { + // Unknown error in network stack QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); - } else if (failed_action->replyStatusCode() == 409) { + } else if (network_error_code == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 114c07ca6..3e0fc1686 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -85,7 +85,7 @@ class ModpackListModel : public QAbstractListModel { public slots: void searchRequestFinished(QList& doc_all); - void searchRequestFailed(QString reason); + void searchRequestFailed(QString reason, int network_error_code); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); protected slots: @@ -98,7 +98,7 @@ class ModpackListModel : public QAbstractListModel { protected: void requestLogo(QString file, QString url); - inline auto getMineVersions() const -> std::list; + inline auto getMineVersions() const -> std::vector; protected: ModrinthPage* m_parent; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 73685d3ba..4798583bd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -68,6 +68,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) m_ui->packView->setModel(m_model); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); @@ -371,11 +372,11 @@ void ModrinthPage::createFilterWidget() connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); - auto response = std::make_shared(); - m_categoriesTask = ModrinthAPI::getModCategories(response); + auto [categoriesTask, response] = ModrinthAPI::getModCategories(); + m_categoriesTask = categoriesTask; connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = ModrinthAPI::loadCategories(response, "modpack"); + auto categories = ModrinthAPI::loadCategories(*response, "modpack"); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); -} \ No newline at end of file +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index d6e983929..c68d01d97 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -54,6 +54,9 @@ 48 + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 32296316f..a1a7390bb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -38,7 +38,7 @@ #include "ModrinthResourcePages.h" #include "ui/pages/modplatform/DataPackModel.h" -#include "ui_ResourcePage.h" +#include "../ui_ResourcePage.h" #include "modplatform/modrinth/ModrinthAPI.h" @@ -165,10 +165,10 @@ std::unique_ptr ModrinthModPage::createFilterWidget() void ModrinthModPage::prepareProviderCategories() { - auto response = std::make_shared(); - m_categoriesTask = ModrinthAPI::getModCategories(response); + auto [categoriesTask, response] = ModrinthAPI::getModCategories(); + m_categoriesTask = categoriesTask; connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = ModrinthAPI::loadModCategories(response); + auto categories = ModrinthAPI::loadModCategories(*response); m_filter_widget->setCategories(categories); }); m_categoriesTask->start(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 8e82dd848..af2aed6d2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -37,6 +37,7 @@ #include "Application.h" #include "BuildConfig.h" #include "Json.h" +#include "settings/SettingsObject.h" #include "net/ApiDownload.h" #include "ui/widgets/ProjectItem.h" @@ -156,23 +157,25 @@ void Technic::ListModel::performSearch() if (!clientId.isEmpty()) { searchUrl += "?cid=" + clientId; } - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(searchUrl)); + netJob->addNetAction(action); jobPtr = netJob; jobPtr->start(); - connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + connect(netJob.get(), &NetJob::succeeded, this, [this, response] { searchRequestFinished(response); }); connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } -void Technic::ListModel::searchRequestFinished() +void Technic::ListModel::searchRequestFinished(QByteArray* responsePtr) { + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); jobPtr.reset(); QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Technic at" << parse_error.offset - << "reason:" << parse_error.errorString(); - qWarning() << *response; + qWarning() << "Error while parsing JSON response from Technic at" << parse_error.offset << "reason:" << parse_error.errorString(); + qWarning() << response; return; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index 4979000e9..872f8b5d6 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -62,7 +62,7 @@ class ListModel : public QAbstractListModel { Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } private slots: - void searchRequestFinished(); + void searchRequestFinished(QByteArray* responsePtr); void searchRequestFailed(); void logoFailed(QString logo); @@ -86,7 +86,6 @@ class ListModel : public QAbstractListModel { Single, } searchMode = List; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); }; } // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index d5ed18696..0858d6397 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -61,6 +61,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); @@ -162,9 +165,12 @@ void TechnicPage::suggestCurrent() auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); - connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { + auto [action, responsePtr] = Net::ApiDownload::makeByteArray( + QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD)); + netJob->addNetAction(action); + connect(netJob.get(), &NetJob::succeeded, this, [this, responsePtr, slug] { + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); jobPtr.reset(); if (current.slug != slug) { @@ -172,12 +178,12 @@ void TechnicPage::suggestCurrent() } QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); QJsonObject obj = doc.object(); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Technic at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *response; + qWarning() << response; return; } if (!obj.contains("url")) { @@ -260,9 +266,10 @@ void TechnicPage::metadataLoaded() auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); + auto [action, response] = Net::ApiDownload::makeByteArray(QUrl(url)); + netJob->addNetAction(action); - connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + connect(netJob.get(), &NetJob::succeeded, this, [this, response] { onSolderLoaded(response); }); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); @@ -293,8 +300,10 @@ void TechnicPage::selectVersion() } } -void TechnicPage::onSolderLoaded() +void TechnicPage::onSolderLoaded(QByteArray* responsePtr) { + // NOTE(TheKodeToad): moving the response out to avoid it from being destroyed by jobPtr.reset() + QByteArray response = std::move(*responsePtr); jobPtr.reset(); auto fallback = [this]() { @@ -307,10 +316,10 @@ void TechnicPage::onSolderLoaded() current.versions.clear(); QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*response, &parse_error); + auto doc = QJsonDocument::fromJson(response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at" << parse_error.offset << "reason:" << parse_error.errorString(); - qWarning() << *response; + qWarning() << response; fallback(); return; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index a131a6db1..466be81d4 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -83,7 +83,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); - void onSolderLoaded(); + void onSolderLoaded(QByteArray* responsePtr); void onVersionSelectionChanged(QString data); private: @@ -95,7 +95,6 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { QString selectedVersion; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); ProgressWidget m_fetch_progress; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index f4e75ae12..31936776a 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -31,6 +31,9 @@ 48 + + QAbstractItemView::ScrollPerPixel + diff --git a/launcher/ui/setupwizard/AutoJavaWizardPage.cpp b/launcher/ui/setupwizard/AutoJavaWizardPage.cpp index fd173e71d..06fc9075b 100644 --- a/launcher/ui/setupwizard/AutoJavaWizardPage.cpp +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.cpp @@ -2,6 +2,7 @@ #include "ui_AutoJavaWizardPage.h" #include "Application.h" +#include "settings/SettingsObject.h" AutoJavaWizardPage::AutoJavaWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::AutoJavaWizardPage) { diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 6b8ece9f7..baeab2da8 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -1,5 +1,6 @@ #include "JavaWizardPage.h" #include "Application.h" +#include "settings/SettingsObject.h" #include #include @@ -10,8 +11,6 @@ #include #include -#include - #include "JavaCommon.h" #include "ui/widgets/JavaWizardWidget.h" diff --git a/launcher/ui/setupwizard/LanguageWizardPage.cpp b/launcher/ui/setupwizard/LanguageWizardPage.cpp index 09cdb807e..e9ba36299 100644 --- a/launcher/ui/setupwizard/LanguageWizardPage.cpp +++ b/launcher/ui/setupwizard/LanguageWizardPage.cpp @@ -1,5 +1,6 @@ #include "LanguageWizardPage.h" #include +#include "settings/SettingsObject.h" #include #include diff --git a/launcher/ui/setupwizard/PasteWizardPage.cpp b/launcher/ui/setupwizard/PasteWizardPage.cpp index 777fd3a44..979ec50fd 100644 --- a/launcher/ui/setupwizard/PasteWizardPage.cpp +++ b/launcher/ui/setupwizard/PasteWizardPage.cpp @@ -2,6 +2,7 @@ #include "ui_PasteWizardPage.h" #include "Application.h" +#include "settings/SettingsObject.h" #include "net/PasteUpload.h" PasteWizardPage::PasteWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::PasteWizardPage) diff --git a/launcher/ui/themes/CatPainter.cpp b/launcher/ui/themes/CatPainter.cpp index 7c152fdc9..a4bda0297 100644 --- a/launcher/ui/themes/CatPainter.cpp +++ b/launcher/ui/themes/CatPainter.cpp @@ -19,6 +19,7 @@ #include "ui/themes/CatPainter.h" #include #include "Application.h" +#include "settings/SettingsObject.h" CatPainter::CatPainter(const QString& path, QObject* parent) : QObject(parent) { diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index b69a416d9..89478960d 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -33,6 +33,7 @@ #include "ui/themes/SystemTheme.h" #include "Application.h" +#include "settings/SettingsObject.h" ThemeManager::ThemeManager() { diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp index 5ab622ef9..41a80dc2a 100644 --- a/launcher/ui/widgets/AppearanceWidget.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -43,6 +43,9 @@ #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" +#include +#include "settings/SettingsObject.h" + AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) : QWidget(parent), m_ui(new Ui::AppearanceWidget), m_themesOnly(themesOnly) { @@ -92,7 +95,7 @@ AppearanceWidget::~AppearanceWidget() void AppearanceWidget::applySettings() { - SettingsObjectPtr settings = APPLICATION->settings(); + SettingsObject* settings = APPLICATION->settings(); QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); settings->set("ConsoleFont", consoleFontFamily); settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); @@ -103,7 +106,7 @@ void AppearanceWidget::applySettings() void AppearanceWidget::loadSettings() { - SettingsObjectPtr settings = APPLICATION->settings(); + SettingsObject* settings = APPLICATION->settings(); QString fontFamily = settings->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); m_ui->consoleFont->setCurrentFont(consoleFont); @@ -175,7 +178,7 @@ void AppearanceWidget::loadThemeSettings() m_ui->widgetStyleComboBox->clear(); m_ui->catPackComboBox->clear(); - const SettingsObjectPtr settings = APPLICATION->settings(); + SettingsObject* settings = APPLICATION->settings(); const QString currentIconTheme = settings->get("IconTheme").toString(); const auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); diff --git a/launcher/ui/widgets/AppearanceWidget.h b/launcher/ui/widgets/AppearanceWidget.h index 1fc89af3a..a63c53112 100644 --- a/launcher/ui/widgets/AppearanceWidget.h +++ b/launcher/ui/widgets/AppearanceWidget.h @@ -20,13 +20,9 @@ #pragma once #include -#include -#include #include #include -#include "java/JavaChecker.h" -#include "ui/pages/BasePage.h" class QTextCharFormat; class SettingsObject; diff --git a/launcher/ui/widgets/EnvironmentVariables.cpp b/launcher/ui/widgets/EnvironmentVariables.cpp index 9387ef2e2..33bb00c63 100644 --- a/launcher/ui/widgets/EnvironmentVariables.cpp +++ b/launcher/ui/widgets/EnvironmentVariables.cpp @@ -104,7 +104,7 @@ QMap EnvironmentVariables::value() const QMap result; QTreeWidgetItem* item = ui->list->topLevelItem(0); for (int i = 1; item != nullptr; item = ui->list->topLevelItem(i++)) - result[item->text(0)] = item->text(1); + result[item->text(0).trimmed()] = item->text(1).trimmed(); return result; } diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 1e641c4f9..3bf9fef82 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #include #include @@ -75,7 +76,7 @@ InfoFrame::~InfoFrame() delete ui; } -void InfoFrame::updateWithMod(Mod const& m) +void InfoFrame::updateWithMod(const Mod& m) { if (m.type() == ResourceType::FOLDER) { clear(); @@ -86,9 +87,9 @@ void InfoFrame::updateWithMod(Mod const& m) QString name = ""; QString link = m.homepage(); if (m.name().isEmpty()) - name = m.internal_id(); + name = m.internalId(); else - name = m.name(); + name = renderColorCodes(m.name()); if (link.isEmpty()) text = name; @@ -103,7 +104,7 @@ void InfoFrame::updateWithMod(Mod const& m) if (m.description().isEmpty()) { setDescription(QString()); } else { - setDescription(m.description()); + setDescription(renderColorCodes(m.description())); } setImage(m.icon({ 64, 64 })); @@ -146,11 +147,12 @@ void InfoFrame::updateWithMod(Mod const& m) void InfoFrame::updateWithResource(const Resource& resource) { const QString homepage = resource.homepage(); + auto name = renderColorCodes(resource.name()); if (!homepage.isEmpty()) - setName("" + resource.name() + ""); + setName("" + name + ""); else - setName(resource.name()); + setName(name); setImage(); } @@ -181,10 +183,10 @@ QString InfoFrame::renderColorCodes(QString input) while (it != input.constEnd()) { // is current char § and is there a following char if (*it == u'§' && (it + 1) != input.constEnd()) { - auto const& code = *(++it); // incrementing here! + const auto& code = *(++it); // incrementing here! - auto const color_entry = color_codes_map.constFind(code); - auto const tag_entry = formatting_codes_map.constFind(code); + const auto color_entry = color_codes_map.constFind(code); + const auto tag_entry = formatting_codes_map.constFind(code); if (color_entry != color_codes_map.constEnd()) { // color code html += QString("").arg(color_entry.value()); @@ -269,6 +271,7 @@ void InfoFrame::updateHiddenState() void InfoFrame::setName(QString text) { + resetScroll(); if (text.isEmpty()) { ui->nameLabel->setHidden(true); } else { @@ -418,3 +421,9 @@ void InfoFrame::boxClosed([[maybe_unused]] int result) { m_current_box = nullptr; } + +void InfoFrame::resetScroll() +{ + ui->scrollArea->horizontalScrollBar()->setValue(0); + ui->scrollArea->verticalScrollBar()->setValue(0); +} diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index 20c54e2e5..b2c867cce 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -76,6 +76,7 @@ class InfoFrame : public QFrame { private: void updateHiddenState(); + void resetScroll(); private: Ui::InfoFrame* ui; diff --git a/launcher/ui/widgets/InfoFrame.ui b/launcher/ui/widgets/InfoFrame.ui index c4d8c83d3..58abcffde 100644 --- a/launcher/ui/widgets/InfoFrame.ui +++ b/launcher/ui/widgets/InfoFrame.ui @@ -7,7 +7,7 @@ 0 0 527 - 113 + 120 @@ -22,7 +22,7 @@ 120 - + 0 @@ -35,7 +35,7 @@ 0 - + @@ -60,95 +60,120 @@ - - - - + + + + + 0 + 0 + - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - + true - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - + + + + 0 + 0 + 455 + 118 + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 01da7cc48..e13c847d0 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -42,19 +42,20 @@ #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" +#include "HardwareInfo.h" #include "JavaCommon.h" +#include "SysInfo.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "settings/Setting.h" -#include "sys.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/VersionSelectDialog.h" #include "ui/java/InstallJavaDialog.h" #include "ui_JavaSettingsWidget.h" -JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) - : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::JavaSettingsWidget) +JavaSettingsWidget::JavaSettingsWidget(BaseInstance* instance, QWidget* parent) + : QWidget(parent), m_instance(instance), m_ui(new Ui::JavaSettingsWidget) { m_ui->setupUi(this); @@ -79,7 +80,7 @@ JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) m_ui->memoryGroupBox->setCheckable(true); m_ui->javaArgumentsGroupBox->setCheckable(true); - SettingsObjectPtr settings = m_instance->settings(); + SettingsObject* settings = m_instance->settings(); connect(settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, [this, settings] { m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); }); @@ -87,7 +88,7 @@ JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) [this, settings] { m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); }); connect(m_ui->javaDownloadBtn, &QPushButton::clicked, this, [this] { - auto javaDialog = new Java::InstallDialog({}, m_instance.get(), this); + auto javaDialog = new Java::InstallDialog({}, m_instance, this); javaDialog->exec(); }); connect(m_ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { @@ -115,7 +116,7 @@ JavaSettingsWidget::~JavaSettingsWidget() void JavaSettingsWidget::loadSettings() { - SettingsObjectPtr settings; + SettingsObject* settings; if (m_instance != nullptr) settings = m_instance->settings(); @@ -150,6 +151,7 @@ void JavaSettingsWidget::loadSettings() m_ui->maxMemSpinBox->setValue(min); } m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt()); + m_ui->lowMemWarningCheckBox->setChecked(settings->get("LowMemWarning").toBool()); // Java arguments m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); @@ -158,7 +160,7 @@ void JavaSettingsWidget::loadSettings() void JavaSettingsWidget::saveSettings() { - SettingsObjectPtr settings; + SettingsObject* settings; if (m_instance != nullptr) settings = m_instance->settings(); @@ -204,10 +206,12 @@ void JavaSettingsWidget::saveSettings() settings->set("MaxMemAlloc", min); } settings->set("PermGen", m_ui->permGenSpinBox->value()); + settings->set("LowMemWarning", m_ui->lowMemWarningCheckBox->isChecked()); } else { settings->reset("MinMemAlloc"); settings->reset("MaxMemAlloc"); settings->reset("PermGen"); + settings->reset("LowMemWarning"); } // Java arguments @@ -265,7 +269,7 @@ void JavaSettingsWidget::onJavaAutodetect() return; } - VersionSelectDialog versionDialog(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + VersionSelectDialog versionDialog(APPLICATION->javalist(), tr("Select a Java version"), this, true); versionDialog.setResizeOn(2); versionDialog.exec(); @@ -285,7 +289,7 @@ void JavaSettingsWidget::onJavaAutodetect() } void JavaSettingsWidget::updateThresholds() { - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + auto sysMiB = HardwareInfo::totalRamMiB(); unsigned int maxMem = m_ui->maxMemSpinBox->value(); unsigned int minMem = m_ui->minMemSpinBox->value(); diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 21a71fb8b..65154597a 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -48,8 +48,8 @@ class JavaSettingsWidget : public QWidget { Q_OBJECT public: - explicit JavaSettingsWidget(QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, nullptr) {} - explicit JavaSettingsWidget(InstancePtr instance, QWidget* parent = nullptr); + explicit JavaSettingsWidget(QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, parent) {} + explicit JavaSettingsWidget(BaseInstance* instance, QWidget* parent = nullptr); ~JavaSettingsWidget() override; void loadSettings(); @@ -62,7 +62,7 @@ class JavaSettingsWidget : public QWidget { void updateThresholds(); private: - InstancePtr m_instance; + BaseInstance* m_instance; Ui::JavaSettingsWidget* m_ui; unique_qobject_ptr m_checker; }; diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 46f714b76..03d632ad9 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -55,7 +55,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -86,7 +86,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -101,10 +101,10 @@ - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -160,10 +160,10 @@ - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -190,156 +190,166 @@ false - - - + + + + + + + M&inimum Memory Usage: + + + minMemSpinBox + + + + + + + + + + 0 + 0 + + + + The amount of memory Minecraft is started with. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 256 + + + + + + + (-Xms) + + + + + + + + + Ma&ximum Memory Usage: + + + maxMemSpinBox + + + + + + + + + + 0 + 0 + + + + The maximum amount of memory Minecraft is allowed to use. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 1024 + + + + + + + (-Xmx) + + + + + + + + + &PermGen Size: + + + permGenSpinBox + + + + + + + + + + 0 + 0 + + + + The amount of memory available to store loaded Java classes. + + + MiB + + + 4 + + + 1048576 + + + 8 + + + 64 + + + + + + + (-XX:PermSize) + + + + + + + + + - (-XX:PermSize) + Warn when there is not enough memory available - - - - - 0 - 0 - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 1048576 - - - 8 - - - 64 - - - - - - - - 0 - 0 - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - (-Xmx) - - - - - - - - 0 - 0 - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - &PermGen Size: - - - permGenSpinBox - - - - - - - (-Xms) - - - - - - - Ma&ximum Memory Usage: - - - maxMemSpinBox - - - - - - - M&inimum Memory Usage: - - - minMemSpinBox - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + Memory Notice @@ -385,6 +395,7 @@ minMemSpinBox maxMemSpinBox permGenSpinBox + lowMemWarningCheckBox jvmArgsTextBox diff --git a/launcher/ui/widgets/JavaWizardWidget.cpp b/launcher/ui/widgets/JavaWizardWidget.cpp index fb2388cc4..bcf498b6d 100644 --- a/launcher/ui/widgets/JavaWizardWidget.cpp +++ b/launcher/ui/widgets/JavaWizardWidget.cpp @@ -13,8 +13,6 @@ #include #include -#include - #include "DesktopServices.h" #include "FileSystem.h" #include "JavaCommon.h" @@ -29,10 +27,11 @@ #include "Application.h" #include "BuildConfig.h" +#include "HardwareInfo.h" JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) { - m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; + m_availableMemory = HardwareInfo::totalRamMiB(); goodIcon = QIcon::fromTheme("status-good"); yellowIcon = QIcon::fromTheme("status-yellow"); @@ -186,7 +185,7 @@ void JavaWizardWidget::setupUi() void JavaWizardWidget::initialize() { - m_versionWidget->initialize(APPLICATION->javalist().get()); + m_versionWidget->initialize(APPLICATION->javalist()); m_versionWidget->selectSearch(); m_versionWidget->setResizeOn(2); auto s = APPLICATION->settings(); @@ -257,15 +256,12 @@ JavaWizardWidget::ValidationStatus JavaWizardWidget::validate() return ValidationStatus::JavaBad; case QMessageBox::Help: DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); - /* fallthrough */ + [[fallthrough]]; case QMessageBox::No: /* fallthrough */ default: return ValidationStatus::Bad; } - if (button == QMessageBox::No) { - return ValidationStatus::Bad; - } } return ValidationStatus::JavaBad; } break; diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp index 481547b5b..3f35df7b0 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.cpp +++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp @@ -6,6 +6,7 @@ #include #include #include "Application.h" +#include "settings/SettingsObject.h" #include "BuildConfig.h" #include "settings/Setting.h" #include "translations/TranslationsModel.h" @@ -40,7 +41,7 @@ LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent) : QWidget(pare auto translations = APPLICATION->translations(); auto index = translations->selectedIndex(); - languageView->setModel(translations.get()); + languageView->setModel(translations); languageView->setCurrentIndex(index); languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); @@ -62,7 +63,7 @@ void LanguageSelectionWidget::retranslate() QString text = tr("Don't see your language or the quality is poor?
Help us with translations!") .arg(BuildConfig.TRANSLATIONS_URL); helpUsLabel->setText(text); - formatCheckbox->setText(tr("Use system locales")); + formatCheckbox->setText(tr("Use system regional standards")); } void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index deae79cd8..460068bd3 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -48,7 +48,7 @@ #include "minecraft/auth/AccountList.h" #include "settings/Setting.h" -MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent) +MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) { m_ui->setupUi(this); @@ -154,7 +154,7 @@ MinecraftSettingsWidget::~MinecraftSettingsWidget() void MinecraftSettingsWidget::loadSettings() { - SettingsObjectPtr settings; + SettingsObject* settings; if (m_instance != nullptr) settings = m_instance->settings(); @@ -308,7 +308,7 @@ void MinecraftSettingsWidget::loadSettings() void MinecraftSettingsWidget::saveSettings() { - SettingsObjectPtr settings; + SettingsObject* settings; if (m_instance != nullptr) settings = m_instance->settings(); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 0dd8e6ba7..847e05806 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -46,7 +46,7 @@ class MinecraftSettingsWidget; class MinecraftSettingsWidget : public QWidget { public: - MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent = nullptr); + MinecraftSettingsWidget(MinecraftInstance* instance, QWidget* parent = nullptr); ~MinecraftSettingsWidget() override; void loadSettings(); @@ -61,7 +61,7 @@ class MinecraftSettingsWidget : public QWidget { void saveDataPacksPath(); void selectDataPacksFolder(); - MinecraftInstancePtr m_instance; + MinecraftInstance* m_instance; Ui::MinecraftSettingsWidget* m_ui; JavaSettingsWidget* m_javaSettings = nullptr; bool m_quickPlaySingleplayer = false; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 80fb8530d..a063f9660 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -858,32 +858,71 @@ It is most likely you will need to change the path - please refer to the mod's w openGlobalSettingsButton settingsTabs scrollArea + + + windowSizeGroupBox maximizedCheckBox - windowHeightSpinBox windowWidthSpinBox + windowHeightSpinBox closeAfterLaunchCheck quitAfterGameStopCheck + + + consoleSettingsBox showConsoleCheck showConsoleErrorCheck autoCloseConsoleCheck + + + globalDataPacksGroupBox + dataPacksPathEdit + dataPacksPathBrowse + + --> + gameTimeGroupBox showGameTime recordGameTime showGlobalGameTime showGameTimeWithoutDays + + instanceAccountGroupBox instanceAccountSelector + + serverJoinGroupBox serverJoinAddressButton serverJoinAddress worldJoinButton worldsCb + + + loaderGroup + neoForge + forge + fabric + quilt + liteLoader + babric + btaBabric + legacyFabric + ornithe + rift + javaScrollArea scrollArea_2 + + + legacySettingsGroupBox onlineFixes + + + nativeWorkaroundsGroupBox useNativeGLFWCheck lineEditGLFWPath useNativeOpenALCheck lineEditOpenALPath + enableFeralGamemodeCheck enableMangoHud useDiscreteGpuCheck diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 4675b2698..6fab2b2a5 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -248,7 +248,7 @@ void ModFilterWidget::prepareBasicFilter() ui->rift->setChecked(loaders & ModPlatform::Rift); m_filter->loaders = loaders; auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); - m_filter->versions.emplace_front(def); + m_filter->versions.emplace_back(def); ui->versions->setCheckedItems({ def }); ui->version->setCurrentIndex(ui->version->findText(def)); } else { @@ -268,7 +268,7 @@ void ModFilterWidget::onVersionFilterChanged(int) { auto versions = ui->versions->checkedItems(); versions.sort(); - std::list current_list; + std::vector current_list; for (const QString& version : versions) current_list.emplace_back(version); @@ -390,7 +390,7 @@ void ModFilterWidget::onOpenSourceFilterChanged() void ModFilterWidget::onReleaseFilterChanged() { - std::list releases; + std::vector releases; if (ui->releaseCb->isChecked()) releases.push_back(ModPlatform::IndexedVersionType::Release); if (ui->betaCb->isChecked()) diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index f00b98eb0..85deb51dc 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -58,8 +58,8 @@ class ModFilterWidget : public QTabWidget { Q_OBJECT public: struct Filter { - std::list versions; - std::list releases; + std::vector versions; + std::vector releases; ModPlatform::ModLoaderTypes loaders; ModPlatform::Side side; bool hideInstalled; diff --git a/launcher/ui/widgets/ModListView.cpp b/launcher/ui/widgets/ModListView.cpp index a38c7c86a..c2191ca5a 100644 --- a/launcher/ui/widgets/ModListView.cpp +++ b/launcher/ui/widgets/ModListView.cpp @@ -35,6 +35,7 @@ ModListView::ModListView(QWidget* parent) : QTreeView(parent) setDragEnabled(true); setDragDropMode(QAbstractItemView::DropOnly); viewport()->setAcceptDrops(true); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); } void ModListView::setModel(QAbstractItemModel* model) diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 58b092275..ad43e95e4 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include "settings/SettingsObject.h" @@ -59,40 +60,43 @@ class PageEntryFilterModel : public QSortFilterProxyModel { public: - explicit PageEntryFilterModel(QObject* parent = 0) : QSortFilterProxyModel(parent) {} + explicit PageEntryFilterModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override { const QString pattern = filterRegularExpression().pattern(); - const auto model = static_cast(sourceModel()); - const auto page = model->pages().at(sourceRow); - if (!page->shouldDisplay()) + auto* const model = static_cast(sourceModel()); + auto* const page = model->pages().at(sourceRow); + if (!page->shouldDisplay()) { return false; + } // Regular contents check, then check page-filter. return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } }; -PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QWidget(parent) +PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) + : QWidget(parent) + , m_proxyModel(new PageEntryFilterModel(this)) + , m_model(new PageModel(this)) { createUI(); useSidebarStyle(true); - m_model = new PageModel(this); - m_proxyModel = new PageEntryFilterModel(this); int counter = 0; auto pages = pageProvider->getPages(); - for (auto page : pages) { - auto widget = dynamic_cast(page); + for (auto* page : pages) { + auto* widget = dynamic_cast(page); widget->setParent(this); page->stackIndex = m_pageStack->addWidget(widget); page->listIndex = counter; page->setParentContainer(this); counter++; - page->updateExtraInfo = [this](QString id, QString info) { - if (m_currentPage && id == m_currentPage->id()) + page->updateExtraInfo = [this](const QString& id, const QString& info) { + if (m_currentPage && id == m_currentPage->id()) { m_header->setText(m_currentPage->displayName() + info); + } }; } m_model->setPages(pages); @@ -108,13 +112,13 @@ PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, connect(m_pageList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &PageContainer::currentChanged); m_pageStack->setStackingMode(QStackedLayout::StackOne); m_pageList->setFocus(); - selectPage(defaultId); + selectPage(std::move(defaultId)); } bool PageContainer::selectPage(QString pageId) { // now find what we want to have selected... - auto page = m_model->findPageEntryById(pageId); + auto* page = m_model->findPageEntryById(pageId); QModelIndex index; if (page) { index = m_proxyModel->mapFromSource(m_model->index(page->listIndex)); @@ -166,11 +170,12 @@ void PageContainer::createUI() QFont headerLabelFont = m_header->font(); headerLabelFont.setBold(true); const int pointSize = headerLabelFont.pointSize(); - if (pointSize > 0) + if (pointSize > 0) { headerLabelFont.setPointSize(pointSize + 2); + } m_header->setFont(headerLabelFont); - QHBoxLayout* headerHLayout = new QHBoxLayout; + auto* headerHLayout = new QHBoxLayout; const int leftMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin); headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); headerHLayout->addWidget(m_header); @@ -190,11 +195,13 @@ void PageContainer::createUI() void PageContainer::retranslate() { - if (m_currentPage) + if (m_currentPage) { m_header->setText(m_currentPage->displayName()); + } - for (auto page : m_model->pages()) + for (auto* page : m_model->pages()) { page->retranslate(); + } } void PageContainer::addButtons(QWidget* buttons) @@ -236,22 +243,23 @@ void PageContainer::help() { if (m_currentPage) { QString pageId = m_currentPage->helpPage(); - if (pageId.isEmpty()) + if (pageId.isEmpty()) { return; + } DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(pageId))); } } void PageContainer::currentChanged(const QModelIndex& current) { - int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1; + int selectedIndex = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1; - auto* selected = m_model->pages().at(selected_index); + auto* selected = m_model->pages().at(selectedIndex); auto* previous = m_currentPage; emit selectedPageChanged(previous, selected); - showPage(selected_index); + showPage(selectedIndex); } bool PageContainer::prepareToClose() @@ -267,9 +275,10 @@ bool PageContainer::prepareToClose() bool PageContainer::saveAll() { - for (auto page : m_model->pages()) { - if (!page->apply()) + for (auto* page : m_model->pages()) { + if (!page->apply()) { return false; + } } return true; } diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 2c7ca9e39..2ca5e6f08 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -56,8 +56,8 @@ class QGridLayout; class PageContainer : public QWidget, public BasePageContainer { Q_OBJECT public: - explicit PageContainer(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); - virtual ~PageContainer() {} + explicit PageContainer(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = nullptr); + ~PageContainer() override = default; void addButtons(QWidget* buttons); void addButtons(QLayout* buttons); diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 2fd5c97c2..1c4fa3596 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -47,30 +47,31 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->setOpacity(0.4); // Fade out the entire item } // The default icon size will be a square (and height is usually the lower value). - auto icon_width = rect.height(), icon_height = rect.height(); + auto icon_width = rect.height(); int icon_x_margin = (rect.height() - icon_width) / 2; - int icon_y_margin = (rect.height() - icon_height) / 2; if (!opt.icon.isNull()) { // Icon painting + auto icon_height = 0; { auto icon_size = opt.decorationSize; icon_width = icon_size.width(); icon_height = icon_size.height(); - icon_y_margin = (rect.height() - icon_height) / 2; - icon_x_margin = icon_y_margin; // use same margins for consistency + icon_x_margin = (rect.height() - icon_height) / 2; // use same margins for consistency } // Centralize icon with a margin to separate from the other elements int x = rect.x() + icon_x_margin; - int y = rect.y() + icon_y_margin; + int y = rect.y() + icon_x_margin; - if (opt.features & QStyleOptionViewItem::HasCheckIndicator) + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { rect.translate(icon_x_margin / 2, 0); + } // Prevent 'scaling null pixmap' warnings - if (icon_width > 0 && icon_height > 0) + if (icon_width > 0 && icon_height > 0) { opt.icon.paint(painter, x, y, icon_width, icon_height); + } } // Change the rect so that funther painting is easier @@ -142,7 +143,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o description_y -= opt.fontMetrics.height(); // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) - painter->drawText(description_x, description_y, remaining_width, cut_text.size() * opt.fontMetrics.height(), Qt::TextWordWrap, + painter->drawText(description_x, description_y, remaining_width, num_lines * opt.fontMetrics.height(), Qt::TextWordWrap, description); } diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp index 7701d1271..475c3da86 100644 --- a/launcher/ui/widgets/VersionListView.cpp +++ b/launcher/ui/widgets/VersionListView.cpp @@ -44,6 +44,7 @@ VersionListView::VersionListView(QWidget* parent) : QTreeView(parent) { m_emptyString = tr("No versions are currently available."); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); } void VersionListView::rowsInserted(const QModelIndex& parent, int start, int end) diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 040355f4b..01b876146 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -127,9 +127,9 @@ void VersionSelectWidget::closeEvent(QCloseEvent* event) QWidget::closeEvent(event); } -void VersionSelectWidget::loadList() +void VersionSelectWidget::loadList(bool forceReload) { - m_load_task = m_vlist->getLoadTask(); + m_load_task = m_vlist->getLoadTask(forceReload); connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed); connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress); diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index c66d7e98e..ee9dbead7 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -57,7 +57,7 @@ class VersionSelectWidget : public QWidget { void initialize(BaseVersionList* vlist, bool forceLoad = false); //! Starts a task that loads the list. - void loadList(); + void loadList(bool forceReload = false); bool hasVersions() const; BaseVersion::Ptr selectedVersion() const; diff --git a/launcher/updater/PrismExternalUpdater.cpp b/launcher/updater/PrismExternalUpdater.cpp index 69774dc04..00f8404cf 100644 --- a/launcher/updater/PrismExternalUpdater.cpp +++ b/launcher/updater/PrismExternalUpdater.cpp @@ -21,15 +21,15 @@ */ #include "PrismExternalUpdater.h" + #include -#include -#include #include #include #include #include #include #include +#include #include #include "StringUtils.h" @@ -43,42 +43,46 @@ class PrismExternalUpdater::Private { QDir appDir; QDir dataDir; QTimer updateTimer; - bool allowBeta; - bool autoCheck; - double updateInterval; + bool allowBeta{}; + bool autoCheck{}; + double updateInterval{}; QDateTime lastCheck; std::unique_ptr settings; - QWidget* parent; + QWidget* parent{}; }; PrismExternalUpdater::PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir) + : priv(new PrismExternalUpdater::Private()) { - priv = new PrismExternalUpdater::Private(); priv->appDir = QDir(appDir); priv->dataDir = QDir(dataDir); - auto settings_file = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg"); - priv->settings = std::make_unique(settings_file, QSettings::Format::IniFormat); + auto settingsFile = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg"); + priv->settings = std::make_unique(settingsFile, QSettings::Format::IniFormat); priv->allowBeta = priv->settings->value("allow_beta", false).toBool(); - priv->autoCheck = priv->settings->value("auto_check", false).toBool(); - bool interval_ok; + priv->autoCheck = priv->settings->value("auto_check", true).toBool(); + bool intervalOk = false; // default once per day - priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&interval_ok); - if (!interval_ok) + priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&intervalOk); + if (!intervalOk) { priv->updateInterval = 86400; - auto last_check = priv->settings->value("last_check"); - if (!last_check.isNull() && last_check.isValid()) { - priv->lastCheck = QDateTime::fromString(last_check.toString(), Qt::ISODate); + } + if (const auto lastCheck = priv->settings->value("last_check"); !lastCheck.isNull() && lastCheck.isValid()) { + priv->lastCheck = QDateTime::fromString(lastCheck.toString(), Qt::ISODate); } priv->parent = parent; connectTimer(); resetAutoCheckTimer(); + if (priv->updateInterval == 0) { // "On Launch" + checkForUpdates(false); + } } PrismExternalUpdater::~PrismExternalUpdater() { - if (priv->updateTimer.isActive()) + if (priv->updateTimer.isActive()) { priv->updateTimer.stop(); + } disconnectTimer(); priv->settings->sync(); delete priv; @@ -89,33 +93,36 @@ void PrismExternalUpdater::checkForUpdates() checkForUpdates(true); } -void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) +void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) const { QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); + progress.setMinimumDuration(0); // Appear immediately without waiting progress.setCancelButton(nullptr); progress.adjustSize(); - progress.show(); + if (triggeredByUser) { + progress.show(); + } QCoreApplication::processEvents(); QProcess proc; - auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); -#if defined Q_OS_WIN32 - exe_name.append(".exe"); + auto exeName = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#ifdef Q_OS_WIN32 + exeName.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else - exe_name = QString("bin/%1").arg(exe_name); + exeName = QString("bin/%1").arg(exeName); #endif QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" }; - if (priv->allowBeta) + if (priv->allowBeta) { args.append("--pre-release"); + } - proc.start(priv->appDir.absoluteFilePath(exe_name), args); - auto result_start = proc.waitForStarted(5000); - if (!result_start) { + proc.start(priv->appDir.absoluteFilePath(exeName), args); + if (auto resultStart = proc.waitForStarted(5000); !resultStart) { auto err = proc.error(); qDebug() << "Failed to start updater after 5 seconds." << "reason:" << err << proc.errorString(); @@ -133,8 +140,7 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) } QCoreApplication::processEvents(); - auto result_finished = proc.waitForFinished(60000); - if (!result_finished) { + if (auto resultFinished = proc.waitForFinished(60000); !resultFinished) { proc.kill(); auto err = proc.error(); auto output = proc.readAll(); @@ -154,15 +160,15 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) return; } - auto exit_code = proc.exitCode(); + auto exitCode = proc.exitCode(); - auto std_output = proc.readAllStandardOutput(); - auto std_error = proc.readAllStandardError(); + auto stdOutput = proc.readAllStandardOutput(); + auto stdError = proc.readAllStandardError(); - progress.hide(); + progress.cancel(); QCoreApplication::processEvents(); - switch (exit_code) { + switch (exitCode) { case 0: // no update available if (triggeredByUser) { @@ -177,10 +183,10 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) case 1: // there was an error { - qDebug() << "Updater subprocess error" << qPrintable(std_error); + qDebug() << "Updater subprocess error" << qPrintable(stdError); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Check Error"), tr("There was an error running the update check."), QMessageBox::Ok, priv->parent); - msgBox.setDetailedText(QString(std_error)); + msgBox.setDetailedText(QString(stdError)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); @@ -189,28 +195,27 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) case 100: // update available { - auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n'); - auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n'); - auto [third_line, release_notes] = StringUtils::splitFirst(remainder2, '\n'); - auto version_name = StringUtils::splitFirst(first_line, ": ").second.trimmed(); - auto version_tag = StringUtils::splitFirst(second_line, ": ").second.trimmed(); - auto release_timestamp = QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second.trimmed(), Qt::ISODate); - qDebug() << "Update available:" << version_name << version_tag << release_timestamp; - qDebug() << "Update release notes:" << release_notes; + auto [firstLine, remainder1] = StringUtils::splitFirst(stdOutput, '\n'); + auto [secondLine, remainder2] = StringUtils::splitFirst(remainder1, '\n'); + auto [thirdLine, releaseNotes] = StringUtils::splitFirst(remainder2, '\n'); + auto versionName = StringUtils::splitFirst(firstLine, ": ").second.trimmed(); + auto versionTag = StringUtils::splitFirst(secondLine, ": ").second.trimmed(); + auto releaseTimestamp = QDateTime::fromString(StringUtils::splitFirst(thirdLine, ": ").second.trimmed(), Qt::ISODate); + qDebug() << "Update available:" << versionName << versionTag << releaseTimestamp; + qDebug() << "Update release notes:" << releaseNotes; - offerUpdate(version_name, version_tag, release_notes); + offerUpdate(versionName, versionTag, releaseNotes, triggeredByUser); } break; default: // unknown error code { - qDebug() << "Updater exited with unknown code" << exit_code; - auto msgBox = - QMessageBox(QMessageBox::Information, tr("Unknown Update Error"), - tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exit_code)), - QMessageBox::Ok, priv->parent); - auto detail_txt = tr("StdOut: %1\nStdErr: %2").arg(QString(std_output)).arg(QString(std_error)); - msgBox.setDetailedText(detail_txt); + qDebug() << "Updater exited with unknown code" << exitCode; + auto msgBox = QMessageBox(QMessageBox::Information, tr("Unknown Update Error"), + tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exitCode)), + QMessageBox::Ok, priv->parent); + auto detailTxt = tr("StdOut: %1\nStdErr: %2").arg(QString(stdOutput)).arg(QString(stdError)); + msgBox.setDetailedText(detailTxt); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); @@ -260,23 +265,27 @@ void PrismExternalUpdater::setBetaAllowed(bool allowed) priv->settings->sync(); } -void PrismExternalUpdater::resetAutoCheckTimer() +void PrismExternalUpdater::resetAutoCheckTimer() const { if (priv->autoCheck && priv->updateInterval > 0) { - int timeoutDuration = 0; auto now = QDateTime::currentDateTime(); + + qint64 timeoutMs = 0; + if (priv->lastCheck.isValid()) { - auto diff = priv->lastCheck.secsTo(now); - auto secs_left = priv->updateInterval - diff; - if (secs_left < 0) - secs_left = 0; - timeoutDuration = secs_left * 1000; // to msec + const qint64 diff = priv->lastCheck.secsTo(now); + const qint64 secsLeft = std::max(priv->updateInterval - diff, 0); + timeoutMs = secsLeft * 1000; } - qDebug() << "Auto update timer starting," << timeoutDuration / 1000 << "seconds left"; - priv->updateTimer.start(timeoutDuration); + + timeoutMs = std::min(timeoutMs, static_cast(INT_MAX)); + + qDebug() << "Auto update timer starting," << timeoutMs / 1000 << "seconds left"; + priv->updateTimer.start(static_cast(timeoutMs)); } else { - if (priv->updateTimer.isActive()) + if (priv->updateTimer.isActive()) { priv->updateTimer.stop(); + } } } @@ -290,68 +299,72 @@ void PrismExternalUpdater::disconnectTimer() disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); } -void PrismExternalUpdater::autoCheckTimerFired() +void PrismExternalUpdater::autoCheckTimerFired() const { qDebug() << "Auto update Timer fired"; checkForUpdates(false); } -void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes) +void PrismExternalUpdater::offerUpdate(const QString& versionName, + const QString& versionTag, + const QString& releaseNotes, + const bool triggeredByUser) const { priv->settings->beginGroup("skip"); - auto should_skip = priv->settings->value(version_tag, false).toBool(); + auto shouldSkip = !triggeredByUser && priv->settings->value(versionTag, false).toBool(); priv->settings->endGroup(); - if (should_skip) { - auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."), - QMessageBox::Ok, priv->parent); - msgBox.setMinimumWidth(460); - msgBox.adjustSize(); - msgBox.exec(); + if (shouldSkip) { + if (triggeredByUser) { + auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."), + QMessageBox::Ok, priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } return; } - UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), version_name, release_notes); + UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), versionName, releaseNotes); auto result = dlg.exec(); qDebug() << "offer dlg result" << result; - switch (result) { - case UpdateAvailableDialog::Install: { - performUpdate(version_tag); - return; - } - case UpdateAvailableDialog::Skip: { - priv->settings->beginGroup("skip"); - priv->settings->setValue(version_tag, true); - priv->settings->endGroup(); - priv->settings->sync(); - return; - } - case UpdateAvailableDialog::DontInstall: { - return; + + priv->settings->beginGroup("skip"); + if (result == UpdateAvailableDialog::Skip) { + priv->settings->setValue(versionTag, true); + } else { + if (result == UpdateAvailableDialog::Install) { + performUpdate(versionTag); } + priv->settings->remove(versionTag); } + priv->settings->endGroup(); + priv->settings->sync(); } -void PrismExternalUpdater::performUpdate(const QString& version_tag) +void PrismExternalUpdater::performUpdate(const QString& versionTag) const { QProcess proc; - auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); -#if defined Q_OS_WIN32 - exe_name.append(".exe"); + auto exeName = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#ifdef Q_OS_WIN32 + exeName.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else - exe_name = QString("bin/%1").arg(exe_name); + exeName = QString("bin/%1").arg(exeName); #endif - QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", version_tag }; - if (priv->allowBeta) + QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", versionTag }; + if (priv->allowBeta) { args.append("--pre-release"); + } - auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args); + proc.setProgram(priv->appDir.absoluteFilePath(exeName)); + proc.setArguments(args); + auto result = proc.startDetached(); if (!result) { qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); } diff --git a/launcher/updater/PrismExternalUpdater.h b/launcher/updater/PrismExternalUpdater.h index b88676028..b3f284b33 100644 --- a/launcher/updater/PrismExternalUpdater.h +++ b/launcher/updater/PrismExternalUpdater.h @@ -22,12 +22,10 @@ #pragma once -#include - #include "ExternalUpdater.h" /*! - * An implementation for the updater on windows and linux that uses out external updater. + * An implementation for the updater on Windows and linux that uses out external updater. */ class PrismExternalUpdater : public ExternalUpdater { @@ -41,7 +39,7 @@ class PrismExternalUpdater : public ExternalUpdater { * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; - void checkForUpdates(bool triggeredByUser); + void checkForUpdates(bool triggeredByUser) const; /*! * Indicates whether or not to check for updates automatically. @@ -62,7 +60,7 @@ class PrismExternalUpdater : public ExternalUpdater { * Set whether or not to check for updates automatically. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow - * reverting this property without kicking off a schedule change immediately." + * reverting this property without kicking off a schedule change immediately. */ void setAutomaticallyChecksForUpdates(bool check) override; @@ -70,7 +68,7 @@ class PrismExternalUpdater : public ExternalUpdater { * Set the current automatic update check interval in seconds. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow - * reverting this property without kicking off a schedule change immediately." + * reverting this property without kicking off a schedule change immediately. */ void setUpdateCheckInterval(double seconds) override; @@ -79,15 +77,15 @@ class PrismExternalUpdater : public ExternalUpdater { */ void setBetaAllowed(bool allowed) override; - void resetAutoCheckTimer(); + void resetAutoCheckTimer() const; void disconnectTimer(); void connectTimer(); - void offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes); - void performUpdate(const QString& version_tag); + void offerUpdate(const QString& versionName, const QString& versionTag, const QString& releaseNotes, bool triggeredByUser) const; + void performUpdate(const QString& versionTag) const; public slots: - void autoCheckTimerFired(); + void autoCheckTimerFired() const; private: class Private; diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index d8e6a2469..6ad60a7c7 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -40,16 +40,6 @@ #include #include -#include - -#if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include "console/WindowsConsole.h" -#endif - #include namespace fs = std::filesystem; @@ -86,12 +76,6 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { -#if defined Q_OS_WIN32 - // attach the parent console if stdout not already captured - if (AttachWindowsConsole()) { - consoleAttached = true; - } -#endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater"); @@ -200,12 +184,13 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage(tr("The launcher data folder is not writable!"), - tr("The updater couldn't create a log file - the data folder is not writable.\n" + tr("The updater couldn't create a log file - %1.\n" "\n" "Make sure you have write permissions to the data folder.\n" - "(%1)\n" + "(%2)\n" "\n" "The updater cannot continue until you fix this problem.") + .arg(logFile->errorString()) .arg(m_dataPath)); return; } @@ -293,7 +278,7 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar } { // network - m_network = makeShared(new QNetworkAccessManager()); + m_network = std::make_unique(); qDebug() << "Detecting proxy settings..."; QNetworkProxy proxy = QNetworkProxy::applicationProxy(); m_network->setProxy(proxy); @@ -382,16 +367,6 @@ PrismUpdaterApp::~PrismUpdaterApp() qDebug() << "updater 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); - FreeConsole(); - } -#endif } void PrismUpdaterApp::fail(const QString& reason) @@ -792,7 +767,7 @@ QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset) qDebug() << "downloading" << file_url << "to" << out_file_path; auto download = Net::Download::makeFile(file_url, out_file_path); - download->setNetwork(m_network); + download->setNetwork(m_network.get()); auto progress_dialog = ProgressDialog(); progress_dialog.adjustSize(); @@ -1161,20 +1136,19 @@ void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page) { int per_page = 30; auto page_url = QString("%1?per_page=%2&page=%3").arg(api_url).arg(QString::number(per_page)).arg(QString::number(page)); - auto response = std::make_shared(); - auto download = Net::Download::makeByteArray(page_url, response); - download->setNetwork(m_network); + auto [download, response] = Net::Download::makeByteArray(page_url); + download->setNetwork(m_network.get()); m_current_url = page_url; - auto github_api_headers = new Net::RawHeaderProxy(); + auto github_api_headers = std::make_unique(); github_api_headers->addHeaders({ { "Accept", "application/vnd.github+json" }, { "X-GitHub-Api-Version", "2022-11-28" }, }); - download->addHeaderProxy(github_api_headers); + download->addHeaderProxy(std::move(github_api_headers)); connect(download.get(), &Net::Download::succeeded, this, [this, response, per_page, api_url, page]() { - int num_found = parseReleasePage(response.get()); + int num_found = parseReleasePage(response); if (!(num_found < per_page)) { // there may be more, fetch next page downloadReleasePage(api_url, page + 1); } else { @@ -1186,8 +1160,6 @@ void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page) m_current_task.reset(download); connect(download.get(), &Net::Download::finished, this, [this]() { qDebug() << "Download" << m_current_task->getUid().toString() << "finished"; - m_current_task.reset(); - m_current_url = ""; }); QCoreApplication::processEvents(); diff --git a/launcher/updater/prismupdater/PrismUpdater.h b/launcher/updater/prismupdater/PrismUpdater.h index a904cbb6f..5f4baec64 100644 --- a/launcher/updater/prismupdater/PrismUpdater.h +++ b/launcher/updater/prismupdater/PrismUpdater.h @@ -46,7 +46,6 @@ #include "GitHubRelease.h" class PrismUpdaterApp : public QApplication { - // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized, Aborted }; @@ -128,7 +127,7 @@ class PrismUpdaterApp : public QApplication { GitHubRelease m_install_release; Status m_status = Status::Starting; - shared_qobject_ptr m_network; + std::unique_ptr m_network; QString m_current_url; Task::Ptr m_current_task; QList m_releases; diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp index eab3e6bbb..31e1b10aa 100644 --- a/launcher/updater/prismupdater/UpdaterDialogs.cpp +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -95,7 +95,7 @@ GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item) return release; } -void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* /*previous*/) { GitHubRelease release = getRelease(current); QString body = markdownToHTML(release.body.toUtf8()); @@ -166,7 +166,7 @@ GitHubReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item) return selected_asset; } -void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* /*previous*/) { GitHubReleaseAsset asset = getAsset(current); m_selectedAsset = asset; diff --git a/launcher/updater/prismupdater/updater_main.cpp b/launcher/updater/prismupdater/updater_main.cpp index 89c1d1198..ddc38d5cd 100644 --- a/launcher/updater/prismupdater/updater_main.cpp +++ b/launcher/updater/prismupdater/updater_main.cpp @@ -21,8 +21,18 @@ */ #include "PrismUpdater.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 if stdout not already captured + console::WindowsConsoleGuard _consoleGuard; +#endif + PrismUpdaterApp wUpApp(argc, argv); switch (wUpApp.status()) { diff --git a/libraries/README.md b/libraries/README.md index df4d251f7..e15d80eba 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -89,11 +89,13 @@ Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiad Available either under LGPL version 2.1 or later. -## systeminfo +## tomlplusplus -A Prism Launcher-specific library for probing system information. +A TOML language parser. Used by Forge 1.14+ to store mod metadata. -Apache 2.0 +See [github repo](https://github.com/marzer/tomlplusplus). + +Licenced under the MIT licence. ## qdcss diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index 531449ba1..353893361 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit 531449ba1c930c98e0bcf5d332b237a8566f9d78 +Subproject commit 3538933614059f0f44388a2b16f3db25ce42285b diff --git a/libraries/systeminfo/CMakeLists.txt b/libraries/systeminfo/CMakeLists.txt deleted file mode 100644 index e091637cf..000000000 --- a/libraries/systeminfo/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -project(systeminfo) - -if(Launcher_QT_VERSION_MAJOR EQUAL 6) - find_package(Qt6 COMPONENTS Core REQUIRED) -endif() - -set(systeminfo_SOURCES -include/sys.h -include/distroutils.h -src/distroutils.cpp -) - -if (WIN32) - list(APPEND systeminfo_SOURCES src/sys_win32.cpp) -elseif (UNIX) - if(APPLE) - list(APPEND systeminfo_SOURCES src/sys_apple.cpp) - else() - list(APPEND systeminfo_SOURCES src/sys_unix.cpp) - endif() -endif() - -add_library(systeminfo STATIC ${systeminfo_SOURCES}) -target_link_libraries(systeminfo Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network ${systeminfo_LIBS}) -target_include_directories(systeminfo PUBLIC include) - -ecm_add_test(src/sys_test.cpp LINK_LIBRARIES systeminfo Qt${QT_VERSION_MAJOR}::Test TEST_NAME sys) diff --git a/libraries/systeminfo/include/distroutils.h b/libraries/systeminfo/include/distroutils.h deleted file mode 100644 index caa2688cf..000000000 --- a/libraries/systeminfo/include/distroutils.h +++ /dev/null @@ -1,22 +0,0 @@ -#include -#include "sys.h" - -namespace Sys { -struct LsbInfo { - QString distributor; - QString version; - QString description; - QString codename; -}; - -bool main_lsb_info(LsbInfo& out); -bool fallback_lsb_info(Sys::LsbInfo& out); -void lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out); -Sys::DistributionInfo read_lsb_release(); - -QString _extract_distribution(const QString& x); -QString _extract_version(const QString& x); -Sys::DistributionInfo read_legacy_release(); - -Sys::DistributionInfo read_os_release(); -} // namespace Sys diff --git a/libraries/systeminfo/include/sys.h b/libraries/systeminfo/include/sys.h deleted file mode 100644 index dfebbe90b..000000000 --- a/libraries/systeminfo/include/sys.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include - -namespace Sys { -const uint64_t mebibyte = 1024ull * 1024ull; - -enum class KernelType { Undetermined, Windows, Darwin, Linux }; - -struct KernelInfo { - QString kernelName; - QString kernelVersion; - - KernelType kernelType = KernelType::Undetermined; - int kernelMajor = 0; - int kernelMinor = 0; - int kernelPatch = 0; - bool isCursed = false; -}; - -KernelInfo getKernelInfo(); - -struct DistributionInfo { - DistributionInfo operator+(const DistributionInfo& rhs) const - { - DistributionInfo out; - if (!distributionName.isEmpty()) { - out.distributionName = distributionName; - } else { - out.distributionName = rhs.distributionName; - } - if (!distributionVersion.isEmpty()) { - out.distributionVersion = distributionVersion; - } else { - out.distributionVersion = rhs.distributionVersion; - } - return out; - } - QString distributionName; - QString distributionVersion; -}; - -DistributionInfo getDistributionInfo(); - -uint64_t getSystemRam(); -} // namespace Sys diff --git a/libraries/systeminfo/src/distroutils.cpp b/libraries/systeminfo/src/distroutils.cpp deleted file mode 100644 index 294dfadbb..000000000 --- a/libraries/systeminfo/src/distroutils.cpp +++ /dev/null @@ -1,242 +0,0 @@ -/* - -Code has been taken from https://github.com/natefoo/lionshead and loosely -translated to C++ laced with Qt. - -MIT License - -Copyright (c) 2017 Nate Coraor - -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 "distroutils.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -static const QRegularExpression s_distoSplitRegex("\\s+"); - -Sys::DistributionInfo Sys::read_os_release() -{ - Sys::DistributionInfo out; - QStringList files = { "/etc/os-release", "/usr/lib/os-release" }; - QString name; - QString version; - for (auto& file : files) { - if (!QFile::exists(file)) { - continue; - } - QSettings settings(file, QSettings::IniFormat); - if (settings.contains("ID")) { - name = settings.value("ID").toString().toLower(); - } else if (settings.contains("NAME")) { - name = settings.value("NAME").toString().toLower(); - } else { - continue; - } - - if (settings.contains("VERSION_ID")) { - version = settings.value("VERSION_ID").toString().toLower(); - } else if (settings.contains("VERSION")) { - version = settings.value("VERSION").toString().toLower(); - } - break; - } - if (name.isEmpty()) { - return out; - } - out.distributionName = name; - out.distributionVersion = version; - return out; -} - -bool Sys::main_lsb_info(Sys::LsbInfo& out) -{ - int status = 0; - QProcess lsbProcess; - QStringList arguments; - arguments << "-a"; - lsbProcess.start("lsb_release", arguments); - lsbProcess.waitForFinished(); - status = lsbProcess.exitStatus(); - QString output = lsbProcess.readAllStandardOutput(); - qDebug() << output; - lsbProcess.close(); - if (status == 0) { - auto lines = output.split('\n'); - for (auto line : lines) { - int index = line.indexOf(':'); - auto key = line.left(index).trimmed(); - auto value = line.mid(index + 1).toLower().trimmed(); - if (key == "Distributor ID") - out.distributor = value; - else if (key == "Release") - out.version = value; - else if (key == "Description") - out.description = value; - else if (key == "Codename") - out.codename = value; - } - return !out.distributor.isEmpty(); - } - return false; -} - -bool Sys::fallback_lsb_info(Sys::LsbInfo& out) -{ - // running lsb_release failed, try to read the file instead - // /etc/lsb-release format, if the file even exists, is non-standard. - // Only the `lsb_release` command is specified by LSB. Nonetheless, some - // distributions install an /etc/lsb-release as part of the base - // distribution, but `lsb_release` remains optional. - QString file = "/etc/lsb-release"; - if (QFile::exists(file)) { - QSettings settings(file, QSettings::IniFormat); - if (settings.contains("DISTRIB_ID")) { - out.distributor = settings.value("DISTRIB_ID").toString().toLower(); - } - if (settings.contains("DISTRIB_RELEASE")) { - out.version = settings.value("DISTRIB_RELEASE").toString().toLower(); - } - return !out.distributor.isEmpty(); - } - return false; -} - -void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) -{ - QString dist = lsb.distributor; - QString vers = lsb.version; - if (dist.startsWith("redhatenterprise")) { - dist = "rhel"; - } else if (dist == "archlinux") { - dist = "arch"; - } else if (dist.startsWith("suse")) { - if (lsb.description.startsWith("opensuse")) { - dist = "opensuse"; - } else if (lsb.description.startsWith("suse linux enterprise")) { - dist = "sles"; - } - } else if (dist == "debian" and vers == "testing") { - vers = lsb.codename; - } else { - // ubuntu, debian, gentoo, scientific, slackware, ... ? - auto parts = dist.split(s_distoSplitRegex, Qt::SkipEmptyParts); - if (parts.size()) { - dist = parts[0]; - } - } - if (!dist.isEmpty()) { - out.distributionName = dist; - out.distributionVersion = vers; - } -} - -Sys::DistributionInfo Sys::read_lsb_release() -{ - LsbInfo lsb; - if (!main_lsb_info(lsb)) { - if (!fallback_lsb_info(lsb)) { - return Sys::DistributionInfo(); - } - } - Sys::DistributionInfo out; - lsb_postprocess(lsb, out); - return out; -} - -QString Sys::_extract_distribution(const QString& x) -{ - QString release = x.toLower(); - if (release.startsWith("red hat enterprise")) { - return "rhel"; - } - if (release.startsWith("suse linux enterprise")) { - return "sles"; - } - QStringList list = release.split(s_distoSplitRegex, Qt::SkipEmptyParts); - if (list.size()) { - return list[0]; - } - return QString(); -} - -QString Sys::_extract_version(const QString& x) -{ - static const QRegularExpression s_versionishString(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); - QStringList list = x.split(s_distoSplitRegex, Qt::SkipEmptyParts); - for (int i = list.size() - 1; i >= 0; --i) { - QString chunk = list[i]; - if (s_versionishString.match(chunk).hasMatch()) { - return chunk; - } - } - return QString(); -} - -Sys::DistributionInfo Sys::read_legacy_release() -{ - struct checkEntry { - QString file; - std::function extract_distro; - std::function extract_version; - }; - QList checks = { - { "/etc/arch-release", [](const QString&) { return "arch"; }, [](const QString&) { return "rolling"; } }, - { "/etc/slackware-version", &Sys::_extract_distribution, &Sys::_extract_version }, - { QString(), &Sys::_extract_distribution, &Sys::_extract_version }, - { "/etc/debian_version", [](const QString&) { return "debian"; }, [](const QString& x) { return x; } }, - }; - for (auto& check : checks) { - QStringList files; - if (check.file.isNull()) { - QDir etcDir("/etc"); - etcDir.setNameFilters({ "*-release" }); - etcDir.setFilter(QDir::Files | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden); - files = etcDir.entryList(); - } else { - files.append(check.file); - } - for (auto file : files) { - QFile relfile(file); - if (!relfile.open(QIODevice::ReadOnly | QIODevice::Text)) - continue; - QString contents = QString::fromUtf8(relfile.readLine()).trimmed(); - QString dist = check.extract_distro(contents); - QString vers = check.extract_version(contents); - if (!dist.isEmpty()) { - Sys::DistributionInfo out; - out.distributionName = dist; - out.distributionVersion = vers; - return out; - } - } - } - return Sys::DistributionInfo(); -} diff --git a/libraries/systeminfo/src/sys_apple.cpp b/libraries/systeminfo/src/sys_apple.cpp deleted file mode 100644 index 5cf70f1aa..000000000 --- a/libraries/systeminfo/src/sys_apple.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "sys.h" - -#include - -#include -#include -#include - -Sys::KernelInfo Sys::getKernelInfo() -{ - Sys::KernelInfo out; - struct utsname buf; - uname(&buf); - out.kernelType = KernelType::Darwin; - out.kernelName = buf.sysname; - QString release = out.kernelVersion = buf.release; - - // TODO: figure out how to detect cursed-ness (macOS emulated on linux via mad hacks and so on) - out.isCursed = false; - - out.kernelMajor = 0; - out.kernelMinor = 0; - out.kernelPatch = 0; - auto sections = release.split('-'); - if (sections.size() >= 1) { - auto versionParts = sections[0].split('.'); - if (versionParts.size() >= 3) { - out.kernelMajor = versionParts[0].toInt(); - out.kernelMinor = versionParts[1].toInt(); - out.kernelPatch = versionParts[2].toInt(); - } else { - qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); - } - } else { - qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); - } - return out; -} - -#include - -uint64_t Sys::getSystemRam() -{ - uint64_t memsize; - size_t memsizesize = sizeof(memsize); - if (!sysctlbyname("hw.memsize", &memsize, &memsizesize, NULL, 0)) { - return memsize; - } else { - return 0; - } -} - -Sys::DistributionInfo Sys::getDistributionInfo() -{ - DistributionInfo result; - return result; -} diff --git a/libraries/systeminfo/src/sys_test.cpp b/libraries/systeminfo/src/sys_test.cpp deleted file mode 100644 index 50c75eb77..000000000 --- a/libraries/systeminfo/src/sys_test.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include - -#include - -class SysTest : public QObject { - Q_OBJECT - private slots: - - void test_kernelNotNull() - { - auto kinfo = Sys::getKernelInfo(); - QVERIFY(!kinfo.kernelName.isEmpty()); - QVERIFY(kinfo.kernelVersion != "0.0"); - } - /* - void test_systemDistroNotNull() - { - auto kinfo = Sys::getDistributionInfo(); - QVERIFY(!kinfo.distributionName.isEmpty()); - QVERIFY(!kinfo.distributionVersion.isEmpty()); - qDebug() << "Distro: " << kinfo.distributionName << "version" << kinfo.distributionVersion; - } - */ -}; - -QTEST_GUILESS_MAIN(SysTest) - -#include "sys_test.moc" diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp deleted file mode 100644 index 4e075959a..000000000 --- a/libraries/systeminfo/src/sys_unix.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "sys.h" - -#include "distroutils.h" - -#include -#include -#include - -#include -#include -#include - -Sys::KernelInfo Sys::getKernelInfo() -{ - Sys::KernelInfo out; - struct utsname buf; - uname(&buf); - // NOTE: we assume linux here. this needs further elaboration - out.kernelType = KernelType::Linux; - out.kernelName = buf.sysname; - QString release = out.kernelVersion = buf.release; - - // linux binary running on WSL is cursed. - out.isCursed = release.contains("WSL", Qt::CaseInsensitive) || release.contains("Microsoft", Qt::CaseInsensitive); - - out.kernelMajor = 0; - out.kernelMinor = 0; - out.kernelPatch = 0; - auto sections = release.split('-'); - if (sections.size() >= 1) { - auto versionParts = sections[0].split('.'); - if (versionParts.size() >= 3) { - out.kernelMajor = versionParts[0].toInt(); - out.kernelMinor = versionParts[1].toInt(); - out.kernelPatch = versionParts[2].toInt(); - } else { - qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); - } - } else { - qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); - } - return out; -} - -uint64_t Sys::getSystemRam() -{ - std::string token; -#ifdef Q_OS_LINUX - std::ifstream file("/proc/meminfo"); - while (file >> token) { - if (token == "MemTotal:") { - uint64_t mem; - if (file >> mem) { - return mem * 1024ull; - } else { - return 0; - } - } - // ignore rest of the line - file.ignore(std::numeric_limits::max(), '\n'); - } -#elif defined(Q_OS_FREEBSD) - char buff[512]; - FILE* fp = popen("sysctl hw.physmem", "r"); - if (fp != NULL) { - while (fgets(buff, 512, fp) != NULL) { - std::string str(buff); - uint64_t mem = std::stoull(str.substr(12, std::string::npos)); - return mem * 1024ull; - } - } -#endif - return 0; // nothing found -} - -Sys::DistributionInfo Sys::getDistributionInfo() -{ - DistributionInfo systemd_info = read_os_release(); - DistributionInfo lsb_info = read_lsb_release(); - DistributionInfo legacy_info = read_legacy_release(); - DistributionInfo result = systemd_info + lsb_info + legacy_info; - if (result.distributionName.isNull()) { - result.distributionName = "unknown"; - } - if (result.distributionVersion.isNull()) { - if (result.distributionName == "arch") { - result.distributionVersion = "rolling"; - } else { - result.distributionVersion = "unknown"; - } - } - return result; -} diff --git a/libraries/systeminfo/src/sys_win32.cpp b/libraries/systeminfo/src/sys_win32.cpp deleted file mode 100644 index 2627761d1..000000000 --- a/libraries/systeminfo/src/sys_win32.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "sys.h" - -#include - -Sys::KernelInfo Sys::getKernelInfo() -{ - Sys::KernelInfo out; - out.kernelType = KernelType::Windows; - out.kernelName = "Windows"; - OSVERSIONINFOW osvi; - ZeroMemory(&osvi, sizeof(OSVERSIONINFOW)); - osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW); - GetVersionExW(&osvi); - out.kernelVersion = QString("%1.%2").arg(osvi.dwMajorVersion).arg(osvi.dwMinorVersion); - out.kernelMajor = osvi.dwMajorVersion; - out.kernelMinor = osvi.dwMinorVersion; - out.kernelPatch = osvi.dwBuildNumber; - return out; -} - -uint64_t Sys::getSystemRam() -{ - MEMORYSTATUSEX status; - status.dwLength = sizeof(status); - GlobalMemoryStatusEx(&status); - // bytes - return (uint64_t)status.ullTotalPhys; -} - -Sys::DistributionInfo Sys::getDistributionInfo() -{ - DistributionInfo result; - return result; -} diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index 478eb7b3e..829a4843c 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -3,12 +3,12 @@ stdenv, cmake, cmark, - extra-cmake-modules, gamemode, jdk17, kdePackages, libnbtplusplus, ninja, + pkg-config, qrencode, self, stripJavaArchivesHook, @@ -35,6 +35,13 @@ let ] else "unknown"; + + # Remove once https://github.com/NixOS/nixpkgs/pull/518987 lands + extra-cmake-modules = kdePackages.extra-cmake-modules.overrideAttrs (prevAttrs: { + meta = prevAttrs.meta // { + platforms = lib.platforms.all; + }; + }); in stdenv.mkDerivation { @@ -65,6 +72,7 @@ stdenv.mkDerivation { cmake ninja extra-cmake-modules + pkg-config jdk17 stripJavaArchivesHook ]; diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 00752a8c4..23fc04d9f 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -6,6 +6,7 @@ glfw3-minecraft, jdk17, jdk21, + jdk25, jdk8, kdePackages, lib, @@ -34,6 +35,7 @@ controllerSupport ? stdenv.hostPlatform.isLinux, gamemodeSupport ? stdenv.hostPlatform.isLinux, jdks ? [ + jdk25 jdk21 jdk17 jdk8 diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index a4c56e1a0..3afd8a642 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -10,43 +10,48 @@ endif() set(Launcher_CommonName "PrismLauncher") set(Launcher_DisplayName "Prism Launcher") +set(Launcher_AppID "org.prismlauncher.PrismLauncher") +set(Launcher_Domain "prismlauncher.org") +set(Launcher_Git "https://github.com/PrismLauncher/PrismLauncher") set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE) set(Launcher_DisplayName "${Launcher_DisplayName}" PARENT_SCOPE) +set(Launcher_ENVName "PRISMLAUNCHER" PARENT_SCOPE) +set(Launcher_Domain "${Launcher_Domain}" PARENT_SCOPE) +set(Launcher_Git "${Launcher_Git}" PARENT_SCOPE) -set(Launcher_AppID "org.prismlauncher.PrismLauncher") set(Launcher_SVGFileName "${Launcher_AppID}.svg") set(Launcher_Copyright "© 2022-2026 Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") set(Launcher_Copyright_Mac "© 2022-2026 Prism Launcher Contributors, © 2021-2022 PolyMC Contributors and © 2012-2021 MultiMC Contributors" PARENT_SCOPE) set(Launcher_Copyright "${Launcher_Copyright}" PARENT_SCOPE) -set(Launcher_Domain "prismlauncher.org" PARENT_SCOPE) set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_VERSION_NAME}" PARENT_SCOPE) -set(Launcher_ConfigFile "prismlauncher.cfg" PARENT_SCOPE) -set(Launcher_Git "https://github.com/PrismLauncher/PrismLauncher" PARENT_SCOPE) +set(Launcher_ConfigFile "${Launcher_APP_BINARY_NAME}.cfg" PARENT_SCOPE) set(Launcher_AppID "${Launcher_AppID}" PARENT_SCOPE) set(Launcher_SVGFileName "${Launcher_SVGFileName}" PARENT_SCOPE) set(Launcher_Desktop "program_info/${Launcher_AppID}.desktop" PARENT_SCOPE) -set(Launcher_mrpack_MIMEInfo "program_info/modrinth-mrpack-mime.xml" PARENT_SCOPE) +set(Launcher_MIMEInfo "program_info/${Launcher_AppID}.xml" PARENT_SCOPE) set(Launcher_MetaInfo "program_info/${Launcher_AppID}.metainfo.xml" PARENT_SCOPE) set(Launcher_PNG_256 "program_info/${Launcher_AppID}_256.png" PARENT_SCOPE) set(Launcher_SVG "program_info/${Launcher_SVGFileName}" PARENT_SCOPE) -set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" PARENT_SCOPE) -set(Launcher_Branding_MAC_ICON "program_info/PrismLauncher.icon" PARENT_SCOPE) -set(Launcher_Branding_ICO "program_info/prismlauncher.ico") +set(Launcher_Branding_ICNS "program_info/${Launcher_APP_BINARY_NAME}.icns" PARENT_SCOPE) +set(Launcher_Branding_MAC_ICON "program_info/${Launcher_CommonName}.icon" PARENT_SCOPE) +set(Launcher_Branding_ICO "program_info/${Launcher_APP_BINARY_NAME}.ico") set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) -set(Launcher_Branding_WindowsRC "program_info/prismlauncher.rc" PARENT_SCOPE) -set(Launcher_Branding_LogoQRC "program_info/prismlauncher.qrc" PARENT_SCOPE) +set(Launcher_Branding_WindowsRC "program_info/${Launcher_APP_BINARY_NAME}.rc" PARENT_SCOPE) +set(Launcher_Branding_LogoQRC "program_info/${Launcher_APP_BINARY_NAME}.qrc" PARENT_SCOPE) +set(Launcher_Authors "MultiMC & Prism Launcher Contributors") set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) configure_file(${Launcher_AppID}.desktop.in ${Launcher_AppID}.desktop) configure_file(${Launcher_AppID}.metainfo.xml.in ${Launcher_AppID}.metainfo.xml) -configure_file(prismlauncher.rc.in prismlauncher.rc @ONLY) -configure_file(prismlauncher.qrc.in prismlauncher.qrc @ONLY) -configure_file(prismlauncher.manifest.in prismlauncher.manifest @ONLY) -configure_file(prismlauncher.ico prismlauncher.ico COPYONLY) +configure_file(${Launcher_APP_BINARY_NAME}.rc.in ${Launcher_APP_BINARY_NAME}.rc @ONLY) +configure_file(${Launcher_APP_BINARY_NAME}.qrc.in ${Launcher_APP_BINARY_NAME}.qrc @ONLY) +configure_file(${Launcher_APP_BINARY_NAME}.manifest.in ${Launcher_APP_BINARY_NAME}.manifest @ONLY) +configure_file(${Launcher_APP_BINARY_NAME}.ico ${Launcher_APP_BINARY_NAME}.ico COPYONLY) configure_file(${Launcher_SVGFileName} ${Launcher_SVGFileName} COPYONLY) +configure_file(${Launcher_AppID}.mime.xml ${Launcher_AppID}.xml COPYONLY) if(MSVC) set(Launcher_MSVC_Redist_NSIS_Section [=[ @@ -75,13 +80,15 @@ endif() configure_file(win_install.nsi.in win_install.nsi @ONLY) if(SCDOC_FOUND) - set(in_scd "${CMAKE_CURRENT_SOURCE_DIR}/prismlauncher.6.scd") - set(out_man "${CMAKE_CURRENT_BINARY_DIR}/prismlauncher.6") + configure_file(${Launcher_APP_BINARY_NAME}.6.scd.in ${Launcher_APP_BINARY_NAME}.6.scd @ONLY) + + set(in_scd "${CMAKE_CURRENT_BINARY_DIR}/${Launcher_APP_BINARY_NAME}.6.scd") + set(out_man "${CMAKE_CURRENT_BINARY_DIR}/${Launcher_APP_BINARY_NAME}.6") add_custom_command( DEPENDS "${in_scd}" OUTPUT "${out_man}" COMMAND ${SCDOC_SCDOC} < "${in_scd}" > "${out_man}" ) add_custom_target(man ALL DEPENDS ${out_man}) - set(Launcher_ManPage "program_info/prismlauncher.6" PARENT_SCOPE) + set(Launcher_ManPage "program_info/${Launcher_APP_BINARY_NAME}.6" PARENT_SCOPE) endif() diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 74c70c5d1..416ca1b6e 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -10,4 +10,4 @@ Icon=@Launcher_AppID@ Categories=Game;ActionGame;AdventureGame;Simulation;PackageManager; Keywords=game;minecraft;mc; StartupWMClass=@Launcher_CommonName@ -MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/@Launcher_APP_BINARY_NAME@; +MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/prismlauncher;x-scheme-handler/@Launcher_APP_BINARY_NAME@; diff --git a/program_info/modrinth-mrpack-mime.xml b/program_info/org.prismlauncher.PrismLauncher.mime.xml similarity index 100% rename from program_info/modrinth-mrpack-mime.xml rename to program_info/org.prismlauncher.PrismLauncher.mime.xml diff --git a/program_info/prismlauncher.6.scd b/program_info/prismlauncher.6.scd.in similarity index 84% rename from program_info/prismlauncher.6.scd rename to program_info/prismlauncher.6.scd.in index e1ebfff32..2b5f3e483 100644 --- a/program_info/prismlauncher.6.scd +++ b/program_info/prismlauncher.6.scd.in @@ -1,14 +1,14 @@ -prismlauncher(6) +@Launcher_APP_BINARY_NAME@(6) # NAME -prismlauncher - a launcher and instance manager for Minecraft. +@Launcher_APP_BINARY_NAME@ - a launcher and instance manager for Minecraft. # SYNOPSIS -*prismlauncher* [OPTIONS...] +*@Launcher_APP_BINARY_NAME@* [OPTIONS...] # DESCRIPTION @@ -69,14 +69,14 @@ variables, besides other common Qt variables: # BUGS -https://github.com/PrismLauncher/PrismLauncher/issues +@Launcher_BUG_TRACKER_URL@ # RESOURCES -GitHub: https://github.com/PrismLauncher/PrismLauncher +GitHub: @Launcher_Git@ -Main website: https://prismlauncher.org +Main website: https://@Launcher_Domain@ # AUTHORS -Prism Launcher Contributors +@Launcher_Authors@ diff --git a/program_info/prismlauncher.rc.in b/program_info/prismlauncher.rc.in index 8f02341e5..700143182 100644 --- a/program_info/prismlauncher.rc.in +++ b/program_info/prismlauncher.rc.in @@ -3,8 +3,8 @@ #endif #include -IDI_ICON1 ICON DISCARDABLE "prismlauncher.ico" -1 RT_MANIFEST "prismlauncher.manifest" +IDI_ICON1 ICON DISCARDABLE "@Launcher_APP_BINARY_NAME@.ico" +1 RT_MANIFEST "@Launcher_APP_BINARY_NAME@.manifest" VS_VERSION_INFO VERSIONINFO FILEVERSION @Launcher_VERSION_NAME4_COMMA@ @@ -15,7 +15,7 @@ BEGIN BEGIN BLOCK "000004b0" BEGIN - VALUE "CompanyName", "MultiMC & Prism Launcher Contributors" + VALUE "CompanyName", "@Launcher_Authors@" VALUE "FileDescription", "@Launcher_DisplayName@" VALUE "FileVersion", "@Launcher_VERSION_NAME4@" VALUE "ProductName", "@Launcher_DisplayName@" diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index e957f4d36..83335ce9d 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -4,6 +4,7 @@ !include "x64.nsh" +AllowSkipFiles off Unicode true Name "@Launcher_DisplayName@" @@ -397,6 +398,10 @@ Section "@Launcher_DisplayName@" WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@ "URL Protocol" "" WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' + ; Write the URL Handler into registry for prismlauncher import + WriteRegStr HKCU Software\Classes\prismlauncher "URL Protocol" "" + WriteRegStr HKCU Software\Classes\prismlauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' + ; Write the uninstall keys for Windows ; https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key ${GetParameters} $R0 diff --git a/renovate.json b/renovate.json index f9c2c3270..856b2e91c 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,13 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" + ], + "labels": [ + "area: actions", + "complexity: low", + "priority: low", + "type: robot", + "changelog:omit" ] } diff --git a/tests/Library_test.cpp b/tests/Library_test.cpp index ba9283c37..73b6bf4a2 100644 --- a/tests/Library_test.cpp +++ b/tests/Library_test.cpp @@ -49,7 +49,7 @@ class LibraryTest : public QObject { { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file" << jsonFile.fileName() << "for reading!"; + qCritical() << "Failed to open file" << jsonFile.fileName() << "for reading:" << jsonFile.errorString(); return LibraryPtr(); } auto data = jsonFile.readAll(); @@ -73,7 +73,7 @@ class LibraryTest : public QObject { { cache.reset(new HttpMetaCache()); cache->addBase("libraries", QDir("libraries").absolutePath()); - dataDir = QDir(QFINDTESTDATA("testdata/Library")).absolutePath(); + dataDir = QDir(QFINDTESTDATA("testdata/Libraries")).absolutePath(); } void test_legacy() { @@ -119,14 +119,14 @@ class LibraryTest : public QObject { QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); - auto downloads = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); + auto downloads = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(downloads.size(), 0); qDebug() << failedFiles; QCOMPARE(failedFiles.size(), 0); QStringList jar, native, native32, native64; - test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); - QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); + test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); + QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); @@ -217,22 +217,23 @@ class LibraryTest : public QObject { test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; - test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); + test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(jar, {}); QCOMPARE(native, {}); - QCOMPARE(native32, { QFileInfo(QFINDTESTDATA("testdata/Library/testname-testversion-linux-32.jar")).absoluteFilePath() }); - QCOMPARE(native64, { QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); + QCOMPARE(native32, { QFileInfo(QFINDTESTDATA("testdata/Libraries/testname-testversion-linux-32.jar")).absoluteFilePath() }); + QCOMPARE(native64, + { QFileInfo(QFINDTESTDATA("testdata/Libraries") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); QStringList failedFiles; - auto dls = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); + auto dls = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, - { QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); + { QFileInfo(QFINDTESTDATA("testdata/Libraries") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); } } void test_onenine() { RuntimeContext r = dummyContext("osx"); - auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-simple.json")); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); @@ -253,8 +254,8 @@ class LibraryTest : public QObject { test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); - QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); + test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); + QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); @@ -262,7 +263,7 @@ class LibraryTest : public QObject { r.system = "linux"; { QStringList failedFiles; - auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); + auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } @@ -270,12 +271,12 @@ class LibraryTest : public QObject { void test_onenine_local_override() { RuntimeContext r = dummyContext("osx"); - auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-simple.json")); test->setHint("local"); { QStringList jar, native, native32, native64; - test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); - QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); + test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Libraries")); + QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Libraries/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); @@ -283,7 +284,7 @@ class LibraryTest : public QObject { r.system = "linux"; { QStringList failedFiles; - auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); + auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Libraries")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } @@ -291,7 +292,7 @@ class LibraryTest : public QObject { void test_onenine_native() { RuntimeContext r = dummyContext("osx"); - auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-native.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, QStringList()); @@ -309,7 +310,7 @@ class LibraryTest : public QObject { void test_onenine_native_arch() { RuntimeContext r = dummyContext("windows"); - auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native-arch.json")); + auto test = readMojangJson(QFINDTESTDATA("testdata/Libraries/lib-native-arch.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); diff --git a/tests/MojangVersionFormat_test.cpp b/tests/MojangVersionFormat_test.cpp index 3c8d3ffc5..385b75bb8 100644 --- a/tests/MojangVersionFormat_test.cpp +++ b/tests/MojangVersionFormat_test.cpp @@ -10,7 +10,7 @@ class MojangVersionFormatTest : public QObject { { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file '" << jsonFile.fileName() << "' for reading!"; + qWarning() << "Failed to open file" << jsonFile.fileName() << "for reading:" << jsonFile.errorString(); return QJsonDocument(); } auto data = jsonFile.readAll(); @@ -21,7 +21,7 @@ class MojangVersionFormatTest : public QObject { { QFile jsonFile(file); if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - qCritical() << "Failed to open file '" << jsonFile.fileName() << "' for writing!"; + qCritical() << "Failed to open file" << jsonFile.fileName() << "for writing:" << jsonFile.errorString(); return; } auto data = doc.toJson(QJsonDocument::Indented); @@ -33,7 +33,7 @@ class MojangVersionFormatTest : public QObject { private slots: void test_Through_Simple() { - QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9-simple.json")); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/Libraries/1.9-simple.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-simple-passthorugh.json", doc2); @@ -43,7 +43,7 @@ class MojangVersionFormatTest : public QObject { void test_Through() { - QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9.json")); + QJsonDocument doc = readJson(QFINDTESTDATA("testdata/Libraries/1.9.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-passthorugh.json", doc2); diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp index f2201a5e9..26fe5ab16 100644 --- a/tests/ResourceFolderModel_test.cpp +++ b/tests/ResourceFolderModel_test.cpp @@ -69,7 +69,7 @@ class ResourceFolderModelTest : public QObject { void test_1178() { // source - QString source = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); + QString source = QFINDTESTDATA("testdata/Resources/test_folder"); // sanity check QVERIFY(!source.endsWith('/')); @@ -133,7 +133,7 @@ class ResourceFolderModelTest : public QObject { void test_addFromWatch() { - QString source = QFINDTESTDATA("testdata/ResourceFolderModel"); + QString source = QFINDTESTDATA("testdata/Resources"); ModFolderModel model(source, nullptr, false, true); QCOMPARE(model.size(), 0); @@ -150,8 +150,8 @@ class ResourceFolderModelTest : public QObject { void test_removeResource() { - QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); - QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); + QString folder_resource = QFINDTESTDATA("testdata/Resources/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/Resources/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(QDir(tmp.path()), nullptr, false, false); @@ -195,8 +195,8 @@ class ResourceFolderModelTest : public QObject { void test_enable_disable() { - QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); - QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); + QString folder_resource = QFINDTESTDATA("testdata/Resources/test_folder"); + QString file_mod = QFINDTESTDATA("testdata/Resources/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(tmp.path(), nullptr, false, false); @@ -217,8 +217,8 @@ class ResourceFolderModelTest : public QObject { auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1); auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1); - auto id_1 = res_1.internal_id(); - auto id_2 = res_2.internal_id(); + auto id_1 = res_1.internalId(); + auto id_2 = res_2.internalId(); bool initial_enabled_res_2 = res_2.enabled(); bool initial_enabled_res_1 = res_1.enabled(); @@ -236,12 +236,12 @@ class ResourceFolderModelTest : public QObject { qDebug() << "res_1 got successfully toggled again."; QVERIFY(res_1.enabled() == initial_enabled_res_1); - QVERIFY(res_1.internal_id() == id_1); + QVERIFY(res_1.internalId() == id_1); qDebug() << "res_1 got back to its initial state."; QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE)); QVERIFY(res_2.enabled() == initial_enabled_res_2); - QVERIFY(res_2.internal_id() == id_2); + QVERIFY(res_2.internalId() == id_2); } }; diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index 5400d888a..c3a82b83d 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -30,7 +30,7 @@ class ResourcePackParseTest : public QObject { private slots: void test_parseZIP() { - QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + QString source = QFINDTESTDATA("testdata/Resources"); QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack{ QFileInfo(zip_rp) }; @@ -46,7 +46,7 @@ class ResourcePackParseTest : public QObject { void test_parseFolder() { - QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + QString source = QFINDTESTDATA("testdata/Resources"); QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; @@ -60,7 +60,7 @@ class ResourcePackParseTest : public QObject { void test_parseFolder2() { - QString source = QFINDTESTDATA("testdata/ResourcePackParse"); + QString source = QFINDTESTDATA("testdata/Resources"); QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index 541355b70..3e52f4eb9 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -107,7 +107,7 @@ class VersionTest : public QObject { QFile vector_file{ test_vector_dir.absoluteFilePath("test_vectors.txt") }; if (!vector_file.open(QFile::OpenModeFlag::ReadOnly)) { - qCritical() << "Failed to open file '" << vector_file.fileName() << "' for reading!"; + qCritical() << "Failed to open file" << vector_file.fileName() << "for reading:" << vector_file.errorString(); return; } @@ -181,8 +181,33 @@ class VersionTest : public QObject { QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } + + static void test_strict_weak_order() + { + // this tests the strict_weak_order + // https://en.cppreference.com/w/cpp/concepts/strict_weak_order.html + const Version a("1.10 Pre-Release 1"); // this is a pre-relese is before b because ' ' is lower than '-' + const Version b("1.10-pre1"); // this is a pre-release is before c that is an actual release + const Version c("1.10"); + + auto r = [](const Version& a, const Version& b) { return a < b; }; + auto e = [&r](const Version& a, const Version& b) { return !r(a, b) && !r(b, a); }; + + qCritical() << a << b << c; + + // irreflexive + QCOMPARE(r(a, a), false); + QCOMPARE(r(b, b), false); + QCOMPARE(r(c, c), false); + // transitive + QCOMPARE(r(a, b), true); + QCOMPARE(r(b, c), true); + QCOMPARE(r(a, c), true); + // transitive equivalence + QCOMPARE(e(a, b) && e(b, c), e(a, c)); + } }; QTEST_GUILESS_MAIN(VersionTest) -#include "Version_test.moc" +#include "Version_test.moc" \ No newline at end of file diff --git a/tests/testdata/MojangVersionFormat/1.9-simple.json b/tests/testdata/Libraries/1.9-simple.json similarity index 100% rename from tests/testdata/MojangVersionFormat/1.9-simple.json rename to tests/testdata/Libraries/1.9-simple.json diff --git a/tests/testdata/MojangVersionFormat/1.9.json b/tests/testdata/Libraries/1.9.json similarity index 100% rename from tests/testdata/MojangVersionFormat/1.9.json rename to tests/testdata/Libraries/1.9.json diff --git a/tests/testdata/MojangVersionFormat/codecwav-20101023.jar b/tests/testdata/Libraries/codecwav-20101023.jar similarity index 100% rename from tests/testdata/MojangVersionFormat/codecwav-20101023.jar rename to tests/testdata/Libraries/codecwav-20101023.jar diff --git a/tests/testdata/MojangVersionFormat/lib-native-arch.json b/tests/testdata/Libraries/lib-native-arch.json similarity index 100% rename from tests/testdata/MojangVersionFormat/lib-native-arch.json rename to tests/testdata/Libraries/lib-native-arch.json diff --git a/tests/testdata/MojangVersionFormat/lib-native.json b/tests/testdata/Libraries/lib-native.json similarity index 100% rename from tests/testdata/MojangVersionFormat/lib-native.json rename to tests/testdata/Libraries/lib-native.json diff --git a/tests/testdata/MojangVersionFormat/lib-simple.json b/tests/testdata/Libraries/lib-simple.json similarity index 100% rename from tests/testdata/MojangVersionFormat/lib-simple.json rename to tests/testdata/Libraries/lib-simple.json diff --git a/tests/testdata/MojangVersionFormat/testname-testversion-linux-32.jar b/tests/testdata/Libraries/testname-testversion-linux-32.jar similarity index 100% rename from tests/testdata/MojangVersionFormat/testname-testversion-linux-32.jar rename to tests/testdata/Libraries/testname-testversion-linux-32.jar diff --git a/tests/testdata/Library b/tests/testdata/Library deleted file mode 120000 index 0e7a22864..000000000 --- a/tests/testdata/Library +++ /dev/null @@ -1 +0,0 @@ -MojangVersionFormat/ \ No newline at end of file diff --git a/tests/testdata/ResourceFolderModel b/tests/testdata/ResourceFolderModel deleted file mode 120000 index c653d859b..000000000 --- a/tests/testdata/ResourceFolderModel +++ /dev/null @@ -1 +0,0 @@ -ResourcePackParse \ No newline at end of file diff --git a/tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta b/tests/testdata/Resources/another_test_folder/pack.mcmeta similarity index 100% rename from tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta rename to tests/testdata/Resources/another_test_folder/pack.mcmeta diff --git a/tests/testdata/ResourcePackParse/supercoolmod.jar b/tests/testdata/Resources/supercoolmod.jar similarity index 100% rename from tests/testdata/ResourcePackParse/supercoolmod.jar rename to tests/testdata/Resources/supercoolmod.jar diff --git a/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/blah.txt b/tests/testdata/Resources/test_folder/assets/minecraft/textures/blah.txt similarity index 100% rename from tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/blah.txt rename to tests/testdata/Resources/test_folder/assets/minecraft/textures/blah.txt diff --git a/tests/testdata/ResourcePackParse/test_folder/pack.mcmeta b/tests/testdata/Resources/test_folder/pack.mcmeta similarity index 100% rename from tests/testdata/ResourcePackParse/test_folder/pack.mcmeta rename to tests/testdata/Resources/test_folder/pack.mcmeta diff --git a/tests/testdata/ResourcePackParse/test_folder/pack.nfo b/tests/testdata/Resources/test_folder/pack.nfo similarity index 100% rename from tests/testdata/ResourcePackParse/test_folder/pack.nfo rename to tests/testdata/Resources/test_folder/pack.nfo diff --git a/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip b/tests/testdata/Resources/test_resource_pack_idk.zip similarity index 100% rename from tests/testdata/ResourcePackParse/test_resource_pack_idk.zip rename to tests/testdata/Resources/test_resource_pack_idk.zip diff --git a/tests/testdata/Version/test_vectors.txt b/tests/testdata/Version/test_vectors.txt index e6c6507cf..971f23daf 100644 --- a/tests/testdata/Version/test_vectors.txt +++ b/tests/testdata/Version/test_vectors.txt @@ -1,5 +1,5 @@ # Test vector from: -# https://github.com/unascribed/FlexVer/blob/704e12759b6e59220ff888f8bf2ec15b8f8fd969/test/test_vectors.txt +# https://git.sleeping.town/exa/FlexVer/src/branch/trunk/test/test_vectors.txt # # This test file is formatted as " ", seperated by the space character # Implementations should ignore lines starting with "#" and lines that have a length of 0 @@ -61,3 +61,10 @@ a1.1.2 < a1.1.2_01 13w02a < c0.3.0_01 0.6.0-1.18.x < 0.9.beta-1.18.x +# removeLeadingZeroes (#17) +0000.0.0 = 0.0.0 +0000.00.0 = 0.00.0 +0.0.0 = 0.00.0000 +# General leading zeroes +1.0.01 = 1.0.1 +1.0.0001 = 1.0.01