diff --git a/.clang-format b/.clang-format index 114bcba50..a0b93351f 100644 --- a/.clang-format +++ b/.clang-format @@ -5,6 +5,7 @@ AllowShortIfStatementsOnASingleLine: false ColumnLimit: 140 --- Language: Cpp +AccessModifierOffset: -1 AlignConsecutiveMacros: None AlignConsecutiveAssignments: None BraceWrapping: diff --git a/.clang-tidy b/.clang-tidy index 436dcf244..6c489fb50 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,23 @@ Checks: - modernize-use-using - readability-avoid-const-params-in-decls + - misc-unused-parameters, + - readability-identifier-naming -SystemHeaders: false +# ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. + +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: PascalCase } + - { key: readability-identifier-naming.EnumCase, value: PascalCase } + - { key: readability-identifier-naming.FunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase } + - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase } + - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.ClassMemberCase, value: camelCase } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } + - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ } + - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ } + - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE } + - { key: readability-identifier-naming.EnumConstantCase, value: PascalCase } diff --git a/.editorconfig b/.editorconfig index 56166b207..dc60e15e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,20 @@ # top-most EditorConfig file root = true +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,nix}] +indent_size = 2 + # C++ Code Style settings [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] cpp_generate_documentation_comments = doxygen_slash_star + +[CMakeLists.txt] +ij_continuation_indent_size = 4 diff --git a/.envrc b/.envrc index 190b5b2b3..1d11c5354 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ -use flake +use nix watch_file nix/*.nix diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 528b128b1..c7d36db27 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,9 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a + +# reformat codebase +1d468ac35ad88d8c77cc83f25e3704d9bd7df01b + +# format a part of codebase +5c8481a118c8fefbfe901001d7828eaf6866eac4 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cf3b79de7..ea1fbfdd9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,64 +2,64 @@ name: Bug Report description: File a bug report labels: [bug] body: - - type: markdown - attributes: - value: | - If you need help with running Minecraft, please visit us on our Discord before making a bug report. +- type: markdown + attributes: + value: | + If you need help with running Minecraft, please visit us on our Discord before making a bug report. - Before submitting a bug report, please make sure you have read this *entire* form, and that: - * You have read the [Prism Launcher wiki](https://prismlauncher.org/wiki/) and it has not answered your question. - * Your bug is not caused by Minecraft or any mods you have installed. - * Your issue has not been reported before, [make sure to use the search function!](https://github.com/PrismLauncher/PrismLauncher/issues) + Before submitting a bug report, please make sure you have read this *entire* form, and that: + * You have read the [Prism Launcher wiki](https://prismlauncher.org/wiki/) and it has not answered your question. + * Your bug is not caused by Minecraft or any mods you have installed. + * Your issue has not been reported before, [make sure to use the search function!](https://github.com/PrismLauncher/PrismLauncher/issues) - **Do not forget to give your issue a descriptive title.** "Bug in the instance screen" makes it hard to distinguish issues at a glance. - - type: dropdown - attributes: - label: Operating System - description: If you know this bug occurs on multiple operating systems, select all you have tested. - multiple: true - options: - - Windows - - macOS - - Linux - - Other - - type: textarea - attributes: - label: Version of NMC Launcher - description: The version of NMC Launcher used in the bug report. - placeholder: NMC Launcher 5.0 - validations: + **Do not forget to give your issue a descriptive title.** "Bug in the instance screen" makes it hard to distinguish issues at a glance. +- type: dropdown + attributes: + label: Operating System + description: If you know this bug occurs on multiple operating systems, select all you have tested. + multiple: true + options: + - Windows + - macOS + - Linux + - Other +- type: textarea + attributes: + label: Version of Prism Launcher + description: The version of Prism Launcher used in the bug report. + placeholder: Prism Launcher 5.0 + validations: + required: true +- type: textarea + attributes: + label: Version of Qt + description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. + placeholder: Qt 6.3.0 + validations: + required: true +- type: textarea + attributes: + label: Description of bug + description: What did you expect to happen, what happened, and why is it incorrect? + placeholder: The cat button should show a cat, but it showed a dog instead! + validations: + required: true +- type: textarea + attributes: + label: Steps to reproduce + description: A bulleted list, or an exported instance if relevant. + placeholder: "* Press the cat button" + validations: + required: true +- type: textarea + attributes: + label: Suspected cause + description: If you know what could be causing this bug, describe it here. + validations: + required: false +- type: checkboxes + attributes: + label: This issue is unique + options: + - label: I have searched the issue tracker and did not find an issue describing my bug. required: true - - type: textarea - attributes: - label: Version of Qt - description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. - placeholder: Qt 6.3.0 - validations: - required: true - - type: textarea - attributes: - label: Description of bug - description: What did you expect to happen, what happened, and why is it incorrect? - placeholder: The cat button should show a cat, but it showed a dog instead! - validations: - required: true - - type: textarea - attributes: - label: Steps to reproduce - description: A bulleted list, or an exported instance if relevant. - placeholder: "* Press the cat button" - validations: - required: true - - type: textarea - attributes: - label: Suspected cause - description: If you know what could be causing this bug, describe it here. - validations: - required: false - - type: checkboxes - attributes: - label: This issue is unique - options: - - label: I have searched the issue tracker and did not find an issue describing my bug. - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358db..97a55db33 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ blank_issues_enabled: true +contact_links: + - name: Prism Launcher Matrix Support Room + url: https://matrix.to/#/#prism-support:matrix.org + about: Please ask for support here before opening an issue. diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml index b76d5f7ee..4d285047e 100644 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ b/.github/ISSUE_TEMPLATE/rfc.yml @@ -3,66 +3,66 @@ name: Request for Comment (RFC) description: Propose a larger change and start a discussion. labels: [rfc] body: - - type: markdown - attributes: - value: | - ### Use this form to suggest a larger change for NMC Launcher. - - type: textarea - attributes: - label: Goal - description: Short description, 1-2 sentences. - placeholder: Remove the cat from the launcher. - validations: +- type: markdown + attributes: + value: | + ### Use this form to suggest a larger change for Prism Launcher. +- type: textarea + attributes: + label: Goal + description: Short description, 1-2 sentences. + placeholder: Remove the cat from the launcher. + validations: + required: true +- type: textarea + attributes: + label: Motivation + description: | + Introduce the topic. If this is a not-well-known section of Prism Launcher, a detailed explanation of the background is recommended. + Some example points of discussion: + - What specific problems are you facing right now that you're trying to address? + - Are there any previous discussions? Link to them and summarize them (don't force your readers to read them though!). + - Is there any precedent set by other software? If so, link to resources. + placeholder: I don't like cats. I think many users also don't like cats. + validations: + required: true +- type: textarea + attributes: + label: Specification + description: A concrete, thorough explanation of what is being planned. + placeholder: Remove the cat button and all references to the cat from the codebase. Including resource files. + validations: + required: true +- type: textarea + attributes: + label: Drawbacks + description: Carefully consider every possible objection and issue with your proposal. This section should be updated as feedback comes in from discussion. + placeholder: Some users might like cats. + validations: + required: true +- type: textarea + attributes: + label: Unresolved Questions + description: | + Are there any portions of your proposal which need to be discussed with the community before the RFC can proceed? + Be careful here -- an RFC with a lot of remaining questions is likely to be stalled. + If your RFC is mostly unresolved questions and not too much substance, it may not be ready. + placeholder: Do a lot of users care about the cat? + validations: + required: true +- type: textarea + attributes: + label: Alternatives Considered + description: A list of alternatives, that have been considered and offer equal or similar features to the proposed change. + placeholder: Maybe the cat could be replaced with an axolotl? + validations: + required: true +- type: checkboxes + attributes: + label: This suggestion is unique + options: + - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. required: true - - type: textarea - attributes: - label: Motivation - description: | - Introduce the topic. If this is a not-well-known section of NMC Launcher, a detailed explanation of the background is recommended. - Some example points of discussion: - - What specific problems are you facing right now that you're trying to address? - - Are there any previous discussions? Link to them and summarize them (don't force your readers to read them though!). - - Is there any precedent set by other software? If so, link to resources. - placeholder: I don't like cats. I think many users also don't like cats. - validations: - required: true - - type: textarea - attributes: - label: Specification - description: A concrete, thorough explanation of what is being planned. - placeholder: Remove the cat button and all references to the cat from the codebase. Including resource files. - validations: - required: true - - type: textarea - attributes: - label: Drawbacks - description: Carefully consider every possible objection and issue with your proposal. This section should be updated as feedback comes in from discussion. - placeholder: Some users might like cats. - validations: - required: true - - type: textarea - attributes: - label: Unresolved Questions - description: | - Are there any portions of your proposal which need to be discussed with the community before the RFC can proceed? - Be careful here -- an RFC with a lot of remaining questions is likely to be stalled. - If your RFC is mostly unresolved questions and not too much substance, it may not be ready. - placeholder: Do a lot of users care about the cat? - validations: - required: true - - type: textarea - attributes: - label: Alternatives Considered - description: A list of alternatives, that have been considered and offer equal or similar features to the proposed change. - placeholder: Maybe the cat could be replaced with an axolotl? - validations: - required: true - - type: checkboxes - attributes: - label: This suggestion is unique - options: - - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. - required: true - - type: textarea - attributes: - label: You may use the editor below to elaborate further. +- type: textarea + attributes: + label: You may use the editor below to elaborate further. diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml index d2ac5dff9..ddee86b65 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -2,37 +2,37 @@ name: Suggestion description: Make a suggestion labels: [enhancement] body: - - type: markdown - attributes: - value: | - ### Use this form to suggest a feature for NMC Launcher. - - type: input - attributes: - label: Role - description: In what way do you use Prism Launcher that needs this feature? - placeholder: I play modded Minecraft. - validations: +- type: markdown + attributes: + value: | + ### Use this form to suggest a feature for Prism Launcher. +- type: input + attributes: + label: Role + description: In what way do you use Prism Launcher that needs this feature? + placeholder: I play modded Minecraft. + validations: + required: true +- type: input + attributes: + label: Suggestion + description: What do you want Prism Launcher to do? + placeholder: I want the cat button to meow. + validations: + required: true +- type: input + attributes: + label: Benefit + description: Why do you need Prism Launcher to do this? + placeholder: so that I can always hear a cat when I need to. + validations: + required: true +- type: checkboxes + attributes: + label: This suggestion is unique + options: + - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. required: true - - type: input - attributes: - label: Suggestion - description: What do you want NMC Launcher to do? - placeholder: I want the cat button to meow. - validations: - required: true - - type: input - attributes: - label: Benefit - description: Why do you need NMC Launcher to do this? - placeholder: so that I can always hear a cat when I need to. - validations: - required: true - - type: checkboxes - attributes: - label: This suggestion is unique - options: - - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. - required: true - - type: textarea - attributes: - label: You may use the editor below to elaborate further. +- type: textarea + attributes: + label: You may use the editor below to elaborate further. diff --git a/.github/actions/get-merge-commit/action.yml b/.github/actions/get-merge-commit/action.yml deleted file mode 100644 index 534d138e1..000000000 --- a/.github/actions/get-merge-commit/action.yml +++ /dev/null @@ -1,103 +0,0 @@ -# This file incorporates work covered by the following copyright and -# permission notice -# -# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -name: Get merge commit -description: Get a merge commit of a given pull request - -inputs: - repository: - description: Repository containing the pull request - required: false - pull-request-id: - description: ID of a pull request - required: true - -outputs: - merge-commit-sha: - description: Git SHA of a merge commit - value: ${{ steps.query.outputs.merge-commit-sha }} - -runs: - using: composite - - steps: - - name: Wait for GitHub to report merge commit - id: query - shell: bash - env: - GITHUB_REPO: ${{ inputs.repository || github.repository }} - PR_ID: ${{ inputs.pull-request-id }} - # https://github.com/NixOS/nixpkgs/blob/8f77f3600f1ee775b85dc2c72fd842768e486ec9/ci/get-merge-commit.sh - run: | - set -euo pipefail - - log() { - echo "$@" >&2 - } - - # Retry the API query this many times - retryCount=5 - # Start with 5 seconds, but double every retry - retryInterval=5 - - while true; do - log "Checking whether the pull request can be merged" - prInfo=$(gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/$GITHUB_REPO/pulls/$PR_ID") - - # Non-open PRs won't have their mergeability computed no matter what - state=$(jq -r .state <<<"$prInfo") - if [[ "$state" != open ]]; then - log "PR is not open anymore" - exit 1 - fi - - mergeable=$(jq -r .mergeable <<<"$prInfo") - if [[ "$mergeable" == "null" ]]; then - if ((retryCount == 0)); then - log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/" - exit 3 - else - ((retryCount -= 1)) || true - - # null indicates that GitHub is still computing whether it's mergeable - # Wait a couple seconds before trying again - log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)" - sleep "$retryInterval" - - ((retryInterval *= 2)) || true - fi - else - break - fi - done - - if [[ "$mergeable" == "true" ]]; then - echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT" - else - echo "# 🚨 The PR has a merge conflict!" >> "$GITHUB_STEP_SUMMARY" - exit 2 - fi diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index 9a26fde79..4960670bf 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -13,10 +13,6 @@ inputs: description: Name of the uploaded artifact required: true default: Linux - cmake-preset: - description: Base CMake preset previously used for the build - required: true - default: linux qt-version: description: Version of Qt to use required: true @@ -56,82 +52,88 @@ runs: - name: Package AppImage shell: bash env: - VERSION: ${{ inputs.version }} + VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.version }} BUILD_DIR: build INSTALL_APPIMAGE_DIR: install-appdir GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - - mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.sogik.NMCLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.sogik.NMCLauncher.appdata.xml - export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated - - export OUTPUT="NMCLauncher-Linux-$APPIMAGE_ARCH.AppImage" - - chmod +x linuxdeploy-*.AppImage - - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - - cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_*64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" - export LD_LIBRARY_PATH - - chmod +x AppImageUpdate-"$APPIMAGE_ARCH".AppImage - cp AppImageUpdate-"$APPIMAGE_ARCH".AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin - - export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|NMCLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }} if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then - export SIGN=1 - export SIGN_KEY=${{ inputs.gpg-private-key-id }} - mkdir -p ~/.gnupg/ - echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key - gpg --import ~/.gnupg/private.key + echo "$GPG_PRIVATE_KEY" > privkey.asc + gpg --import privkey.asc + gpg --export --armor 9C7A2C9B62603299 > pubkey.asc else echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY fi - ./linuxdeploy-"$APPIMAGE_ARCH".AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.sogik.NMCLauncher.svg + sharun lib4bin \ + --hard-links \ + --with-hooks \ + --dst-dir "$INSTALL_APPIMAGE_DIR" \ + "$INSTALL_APPIMAGE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so + + cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/bin/ + # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! + find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + + + #makes the launcher use portals for file picking + echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" > "$INSTALL_APPIMAGE_DIR"/.env + ln -s org.sogik.NMCLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.sogik.NMCLauncher.appdata.xml + ln -s share/applications/org.sogik.NMCLauncher.desktop "$INSTALL_APPIMAGE_DIR" + ln -s share/icons/hicolor/256x256/apps/org.sogik.NMCLauncher.png "$INSTALL_APPIMAGE_DIR" + mv "$INSTALL_APPIMAGE_DIR"/{sharun,AppRun} + ls -la "$INSTALL_APPIMAGE_DIR" + + if [[ "${{ github.ref_type }}" == "tag" ]]; then + APPIMAGE_DEST="NMCLauncher-Linux-$APPIMAGE_ARCH.AppImage" + else + APPIMAGE_DEST="NMCLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" + fi - mv "NMCLauncher-Linux-$APPIMAGE_ARCH.AppImage" "NMCLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" + mkappimage \ + --updateinformation "gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|NMCLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" \ + "$INSTALL_APPIMAGE_DIR" \ + "$APPIMAGE_DEST" - name: Package portable tarball shell: bash env: BUILD_DIR: build - CMAKE_PRESET: ${{ inputs.cmake-preset }} - INSTALL_PORTABLE_DIR: install-portable run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + + sharun lib4bin \ + --with-hooks \ + --hard-links \ + --dst-dir "$INSTALL_PORTABLE_DIR" \ + "$INSTALL_PORTABLE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so + + # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! + find "$INSTALL_PORTABLE_DIR" -name '*gamemode*' -exec rm {} + - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f -o -type l); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt cd ${{ env.INSTALL_PORTABLE_DIR }} tar -czf ../NMCLauncher-portable.tar.gz * - name: Upload binary tarball - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: NMCLauncher-portable.tar.gz - name: Upload AppImage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage - path: NMCLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage + path: NMCLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage - name: Upload AppImage Zsync - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync - path: NMCLauncher-${{ runner.os }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync + path: NMCLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage.zsync diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml index 89675fbd0..7fec38d4f 100644 --- a/.github/actions/package/macos/action.yml +++ b/.github/actions/package/macos/action.yml @@ -59,7 +59,7 @@ runs: BUILD_DIR: build INSTALL_DIR: install run: | - cmake --install ${{ env.BUILD_DIR }} + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} cd ${{ env.INSTALL_DIR }} chmod +x "NMCLauncher.app/Contents/MacOS/nmclauncher" @@ -90,7 +90,7 @@ runs: --team-id '${{ inputs.apple-notarize-team-id }}' \ --password '${{ inputs.apple-notarize-password }}' - xcrun stapler staple "Prism Launcher.app" + xcrun stapler staple "NMC Launcher.app" else echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY fi @@ -115,7 +115,7 @@ runs: fi - name: Upload binary tarball - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: NMCLauncher.zip diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml index 8e1d34757..ce1f1a84b 100644 --- a/.github/actions/package/windows/action.yml +++ b/.github/actions/package/windows/action.yml @@ -14,14 +14,16 @@ inputs: required: true msystem: description: MSYS2 subsystem to use - required: true - default: false - windows-codesign-cert: - description: Certificate for signing Windows builds - required: false - windows-codesign-password: - description: Password for signing Windows builds required: false + azure-client-id: + description: Client ID for the Azure Signer Application + required: true + azure-tenant-id: + description: Tenant ID for the Azure Signer Application + required: true + azure-subscription-id: + description: Subscription ID for the Azure Signer Application + required: true runs: using: composite @@ -34,7 +36,7 @@ runs: BUILD_DIR: build INSTALL_DIR: install run: | - cmake --install ${{ env.BUILD_DIR }} + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} touch ${{ env.INSTALL_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt @@ -51,23 +53,45 @@ runs: Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - name: Fetch codesign certificate - shell: bash # yes, we are not using MSYS2 or PowerShell here - run: | - echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx - - - name: Sign executable + - name: Emit warning for unsigned builds + if: ${{ env.CI_HAS_ACCESS_TO_AZURE == '' || inputs.azure-client-id == '' }} shell: pwsh - env: - INSTALL_DIR: install run: | - if (Get-Content ./codesign.pfx){ - cd ${{ env.INSTALL_DIR }} - # We ship the exact same executable for portable and non-portable editions, so signing just once is fine - SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com nmclauncher.exe nmclauncher_updater.exe nmclauncher_filelink.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + + - name: Login to Azure + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} + uses: azure/login@v2 + with: + client-id: ${{ inputs.azure-client-id }} + tenant-id: ${{ inputs.azure-tenant-id }} + subscription-id: ${{ inputs.azure-subscription-id }} + + - name: Sign executables + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} + uses: azure/trusted-signing-action@v1 + with: + endpoint: https://eus.codesigning.azure.net/ + trusted-signing-account-name: PrismLauncher + certificate-profile-name: PrismLauncher + + files: | + ${{ github.workspace }}\install\nmclauncher.exe + ${{ github.workspace }}\install\nmclauncher_filelink.exe + ${{ github.workspace }}\install\nmclauncher_updater.exe + + # TODO(@getchoo): Is this all really needed??? + # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md + exclude-environment-credential: true + exclude-workload-identity-credential: true + exclude-managed-identity-credential: true + exclude-shared-token-cache-credential: true + exclude-visual-studio-credential: true + exclude-visual-studio-code-credential: true + exclude-azure-cli-credential: false + exclude-azure-powershell-credential: true + exclude-azure-developer-cli-credential: true + exclude-interactive-browser-credential: true - name: Package (MinGW, portable) if: ${{ inputs.msystem != '' }} @@ -78,7 +102,7 @@ runs: INSTALL_PORTABLE_DIR: install-portable run: | cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - name: Package (MSVC, portable) @@ -90,7 +114,7 @@ runs: INSTALL_PORTABLE_DIR: install-portable run: | cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt @@ -116,28 +140,43 @@ runs: makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - name: Sign installer - shell: pwsh - run: | - if (Get-Content ./codesign.pfx){ - SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com NMCLauncher-Setup.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} + uses: azure/trusted-signing-action@v1 + with: + endpoint: https://eus.codesigning.azure.net/ + trusted-signing-account-name: PrismLauncher + certificate-profile-name: PrismLauncher + + files: | + ${{ github.workspace }}\NMCLauncher-Setup.exe + + # TODO(@getchoo): Is this all really needed??? + # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md + exclude-environment-credential: true + exclude-workload-identity-credential: true + exclude-managed-identity-credential: true + exclude-shared-token-cache-credential: true + exclude-visual-studio-credential: true + exclude-visual-studio-code-credential: true + exclude-azure-cli-credential: false + exclude-azure-powershell-credential: true + exclude-azure-developer-cli-credential: true + exclude-interactive-browser-credential: true - name: Upload binary zip - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: install/** - name: Upload portable zip - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: install-portable/** - name: Upload installer - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: NMCLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }} path: NMCLauncher-Setup.exe diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 405f524c4..a8ecba583 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -1,5 +1,5 @@ name: Setup Dependencies -description: Install and setup dependencies for building NMC Launcher +description: Install and setup dependencies for building Prism Launcher inputs: build-type: @@ -21,7 +21,6 @@ inputs: qt-version: description: Version of Qt to use required: true - default: 6.8.1 outputs: build-type: @@ -42,6 +41,8 @@ runs: - name: Setup macOS dependencies if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/setup-dependencies/macos + with: + build-type: ${{ inputs.build-type }} - name: Setup Windows dependencies if: ${{ runner.os == 'Windows' }} @@ -54,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.18 + uses: hendrikmuhs/ccache-action@v1.2.20 with: variant: sccache create-symlink: ${{ runner.os != 'Windows' }} @@ -76,6 +77,5 @@ runs: with: aqtversion: "==3.1.*" version: ${{ inputs.qt-version }} - arch: ${{ inputs.qt-architecture }} - modules: qt5compat qtimageformats qtnetworkauth + modules: qtimageformats qtnetworkauth cache: ${{ inputs.build-type == 'Debug' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 94c04abe5..753ea19fe 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -10,25 +10,23 @@ runs: sudo apt-get -y update sudo apt-get -y install \ dpkg-dev \ - ninja-build extra-cmake-modules scdoc \ - appstream libxcb-cursor-dev + ninja-build extra-cmake-modules pkg-config scdoc \ + cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ + libxcb-cursor-dev libtomlplusplus-dev - name: Setup AppImage tooling shell: bash + env: + GH_TOKEN: ${{ github.token }} run: | - declare -A appimage_deps - - deb_arch="$(dpkg-architecture -q DEB_HOST_ARCH)" - case "$deb_arch" in + # Determinate AppImage architecture to use + dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" + case "$dpkg_arch" in "amd64") - appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" - appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" - appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + APPIMAGE_ARCH="x86_64" ;; "arm64") - appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-aarch64.AppImage"]="06706ac8189797dccd36bd384105892cb5e6e71f784f4df526cc958adc223cd6 linuxdeploy-aarch64.AppImage" - appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-aarch64.AppImage"]="bf1c24aff6d749b5cf423afad6f15abd4440f81dec1aab95706b25f6667cdcf1 linuxdeploy-plugin-qt-aarch64.AppImage" - appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-aarch64.AppImage"]="cf27f810dfe5eda41f130769e4a4b562b9d93665371c15ebeffb84ee06a41550 AppImageUpdate-aarch64.AppImage" + APPIMAGE_ARCH="aarch64" ;; *) echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" @@ -36,9 +34,20 @@ runs: ;; esac - for url in "${!appimage_deps[@]}"; do - curl -LO "$url" - sha256sum -c - <<< "${appimage_deps[$url]}" - done + gh release download \ + --repo VHSgunzo/sharun \ + --pattern "sharun-$APPIMAGE_ARCH-aio" \ + --output ~/bin/sharun + + # FIXME!: revert this to probonopd/go-appimage once https://github.com/probonopd/go-appimage/pull/377 is merged! + gh release download continuous \ + --repo DioEgizio/go-appimage \ + --pattern "mkappimage-*-$APPIMAGE_ARCH.AppImage" \ + --output ~/bin/mkappimage - sudo apt -y install libopengl0 + gh release download \ + --repo AppImageCommunity/AppImageUpdate \ + --pattern "AppImageUpdate-$APPIMAGE_ARCH.AppImage" \ + --output ~/bin/AppImageUpdate.AppImage + chmod +x ~/bin/* + echo "$HOME/bin" >> "$GITHUB_PATH" diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml index dcbb308c2..a90544be0 100644 --- a/.github/actions/setup-dependencies/macos/action.yml +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -1,5 +1,11 @@ name: Setup macOS dependencies +inputs: + build-type: + description: Type for the build + required: true + default: Debug + runs: using: composite @@ -8,9 +14,34 @@ runs: shell: bash run: | brew update - brew install ninja extra-cmake-modules temurin@17 + brew install ninja extra-cmake-modules temurin@17 mono - name: Set JAVA_HOME shell: bash run: | echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" + + - name: Setup vcpkg cache + if: ${{ inputs.build-type == 'Debug' }} + shell: bash + env: + USERNAME: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ github.token }} + FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + run: | + mono `vcpkg fetch nuget | tail -n 1` \ + sources add \ + -Source "$FEED_URL" \ + -StorePasswordInClearText \ + -Name GitHubPackages \ + -UserName "$USERNAME" \ + -Password "$GITHUB_TOKEN" + mono `vcpkg fetch nuget | tail -n 1` \ + setapikey "$GITHUB_TOKEN" \ + -Source "$FEED_URL" + echo "VCPKG_BINARY_SOURCES=clear;nuget,$FEED_URL,readwrite" >> "$GITHUB_ENV" + + - name: Setup vcpkg environment + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 0a643f583..3da43ec88 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -25,6 +25,40 @@ runs: arch: ${{ inputs.vcvars-arch }} vsversion: 2022 + - name: Setup Java (MSVC) + uses: actions/setup-java@v5 + with: + # NOTE(@getchoo): We should probably stay on Zulu. + # Temurin doesn't have Java 17 builds for WoA + distribution: zulu + java-version: 17 + + - name: Setup vcpkg cache (MSVC) + if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} + shell: pwsh + env: + USERNAME: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ github.token }} + FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + run: | + .$(vcpkg fetch nuget) ` + sources add ` + -Source "$env:FEED_URL" ` + -StorePasswordInClearText ` + -Name GitHubPackages ` + -UserName "$env:USERNAME" ` + -Password "$env:GITHUB_TOKEN" + .$(vcpkg fetch nuget) ` + setapikey "$env:GITHUB_TOKEN" ` + -Source "$env:FEED_URL" + "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" | Out-File -Append $env:GITHUB_ENV + + - name: Setup vcpkg environment (MSVC) + if: ${{ inputs.msystem == '' }} + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" + - name: Setup MSYS2 (MinGW) if: ${{ inputs.msystem != '' }} uses: msys2/setup-msys2@v2 @@ -42,11 +76,11 @@ runs: qt6-base:p qt6-svg:p qt6-imageformats:p - qt6-5compat:p qt6-networkauth:p cmark:p + qrencode:p tomlplusplus:p - quazip-qt6:p + libarchive:p - name: List pacman packages (MinGW) if: ${{ inputs.msystem != '' }} @@ -56,7 +90,7 @@ runs: - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - uses: actions/cache@v4.2.3 + uses: actions/cache@v5.0.2 with: path: '${{ github.workspace }}\.ccache' key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index ecbaf755d..245ea1d21 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -70,7 +70,6 @@ jobs: ' <<< "$PR_JSON")" } >> "$GITHUB_ENV" - - name: Find Blocked/Stacked PRs in body id: pr_ids run: | @@ -252,4 +251,3 @@ jobs: --body "$COMMENT_BODY" \ --create-if-none \ --edit-last - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1173a58d2..1360b7831 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,9 @@ name: Build +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: branches: @@ -58,6 +62,9 @@ on: description: Type of build (Debug or Release) type: string default: Debug + environment: + description: Deployment environment to run under + type: string workflow_dispatch: inputs: build-type: @@ -69,56 +76,61 @@ jobs: build: name: Build (${{ matrix.artifact-name }}) + environment: ${{ inputs.environment || '' }} + + permissions: + # Required for vcpkg binary cache + packages: write + strategy: fail-fast: false matrix: include: - - os: ubuntu-22.04 + - os: ubuntu-24.04 artifact-name: Linux - base-cmake-preset: linux + cmake-preset: linux + qt-version: 6.10.1 - # NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!! - # - # It's not really documented anywhere AFAICT, but upstream Qt binaries - # *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64 - os: ubuntu-24.04-arm artifact-name: Linux-aarch64 - base-cmake-preset: linux + cmake-preset: linux + qt-version: 6.10.1 - os: windows-2022 artifact-name: Windows-MinGW-w64 - base-cmake-preset: windows_mingw + cmake-preset: windows_mingw msystem: CLANG64 vcvars-arch: amd64_x86 - os: windows-11-arm artifact-name: Windows-MinGW-arm64 - base-cmake-preset: windows_mingw + cmake-preset: windows_mingw msystem: CLANGARM64 vcvars-arch: arm64 - os: windows-2022 artifact-name: Windows-MSVC - base-cmake-preset: windows_msvc + cmake-preset: windows_msvc # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! vcvars-arch: amd64 + qt-version: 6.10.1 - os: windows-2022 artifact-name: Windows-Clang - base-cmake-preset: windows_clang - vcvars-arch: amd64 - compiler: clang + cmake-preset: windows_clang + msystem: CLANG64 - - os: windows-2022 + - os: windows-11-arm artifact-name: Windows-MSVC-arm64 - base-cmake-preset: windows_msvc_arm64_cross - vcvars-arch: amd64_arm64 - qt-architecture: win64_msvc2022_arm64_cross_compiled + cmake-preset: windows_msvc + vcvars-arch: arm64 + qt-version: 6.10.1 - - os: macos-14 + - os: macos-26 artifact-name: macOS - base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }} + cmake-preset: macos_universal macosx-deployment-target: 12.0 + qt-version: 6.9.3 runs-on: ${{ matrix.os }} @@ -127,7 +139,13 @@ jobs: shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} env: + ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 + BUILD_PLATFORM: official + BUILD_TYPE: ${{ inputs.build-type || 'Debug' }} + CMAKE_PRESET: ${{ matrix.cmake-preset }} + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} steps: ## @@ -135,7 +153,7 @@ jobs: ## - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: true @@ -143,60 +161,27 @@ jobs: id: setup-dependencies uses: ./.github/actions/setup-dependencies with: - build-type: ${{ inputs.build-type || 'Debug' }} + build-type: ${{ env.BUILD_TYPE }} artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} vcvars-arch: ${{ matrix.vcvars-arch }} - qt-architecture: ${{ matrix.qt-architecture }} - - - name: Install Clang (Windows) - if: matrix.compiler == 'clang' - shell: pwsh - run: | - $llvmInstalled = choco list --local-only llvm - if ($llvmInstalled -match "llvm") { - Write-Host "LLVM already installed, skipping installation" - } else { - choco install llvm -y - } - echo "C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Export CurseForge API Key - run: echo "CURSEFORGE_API_KEY=${{ secrets.CURSEFORGE_API_KEY }}" >> $GITHUB_ENV - - - name: Export MSA Client ID - run: echo "MSA_CLIENT_ID=${{ secrets.MSA_CLIENT_ID }}" >> $GITHUB_ENV + qt-version: ${{ matrix.qt-version }} ## # BUILD ## - - name: List CMake presets (debug) - run: | - echo "Available CMake presets:" - cmake --list-presets=all || true - echo "---" - echo "Expected preset: ${{ matrix.base-cmake-preset }}_${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }}" - - - name: Get CMake preset - id: cmake-preset - env: - BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }} - PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }} + - name: Configure project run: | - echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT" - echo "Using preset: $BASE_CMAKE_PRESET"_"$PRESET_TYPE" + cmake --preset "$CMAKE_PRESET" - - name: Run CMake workflow - env: - CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }} + - name: Run build + run: | + cmake --build --preset "$CMAKE_PRESET" --config "$BUILD_TYPE" - ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 - BUILD_PLATFORM: official - CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} - MSA_CLIENT_ID: ${{ secrets.MSA_CLIENT_ID }} + - name: Run tests run: | - cmake --workflow --preset "$CMAKE_PRESET" + ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE" ## # PACKAGE @@ -215,12 +200,8 @@ jobs: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} - cmake-preset: ${{ steps.cmake-preset.outputs.preset }} qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }} - - name: Package (macOS) if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/package/macos @@ -229,14 +210,6 @@ jobs: build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} - apple-codesign-cert: ${{ secrets.APPLE-CODESIGN-CERT }} - apple-codesign-password: ${{ secrets.APPLE-CODESIGN_PASSWORD }} - apple-codesign-id: ${{ secrets.APPLE-CODESIGN_ID }} - apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} - apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} - apple-notarize-password: ${{ secrets.APPLE-NOTARIZE_PASSWORD }} - sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }} - - name: Package (Windows) if: ${{ runner.os == 'Windows' }} uses: ./.github/actions/package/windows @@ -245,6 +218,3 @@ jobs: build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} - - windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }} - windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 834343b90..1c3ff567e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,5 +1,9 @@ name: "CodeQL Code Scanning" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: branches: @@ -60,7 +64,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: "true" @@ -75,11 +79,12 @@ jobs: uses: ./.github/actions/setup-dependencies with: build-type: Debug + qt-version: 6.10.1 - name: Configure and Build run: | - cmake --preset linux_debug - cmake --build --preset linux_debug + cmake --preset linux + cmake --build --preset linux --config Debug - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml index d37c33761..182e40984 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -38,7 +38,7 @@ jobs: gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ | jq -c --argjson pr "$PR_NUMBER" ' reduce ( .[] | select( - .body | + .body | scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | map(tonumber) | any(.[]; . == $pr) @@ -47,7 +47,7 @@ jobs: ) { echo "deps=$blocked_prs" - echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" + echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" } >> "$GITHUB_OUTPUT" - name: Trigger Blocked PR Workflows for Dependants diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5527ed796..363da4888 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ jobs: uses: ./.github/workflows/build.yml with: build-type: Release + environment: Release secrets: inherit create_release: @@ -20,12 +21,12 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: "true" path: "NMCLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") @@ -34,6 +35,7 @@ jobs: run: | mv ${{ github.workspace }}/NMCLauncher-source NMCLauncher-${{ env.VERSION }} mv NMCLauncher-Linux-Qt6-Portable*/NMCLauncher-portable.tar.gz NMCLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz + mv NMCLauncher-Linux-aarch64-Qt6-Portable*/NMCLauncher-portable.tar.gz NMCLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz mv NMCLauncher-*.AppImage/NMCLauncher-*-x86_64.AppImage NMCLauncher-Linux-x86_64.AppImage mv NMCLauncher-*.AppImage.zsync/NMCLauncher-*-x86_64.AppImage.zsync NMCLauncher-Linux-x86_64.AppImage.zsync mv NMCLauncher-*.AppImage/NMCLauncher-*-aarch64.AppImage NMCLauncher-Linux-aarch64.AppImage @@ -57,17 +59,6 @@ jobs: cd .. done - for d in NMCLauncher-Windows-Clang*; do - cd "${d}" || continue - INST="$(echo -n ${d} | grep -o Setup || true)" - PORT="$(echo -n ${d} | grep -o Portable || true)" - NAME="NMCLauncher-Windows-Clang" - test -z "${PORT}" || NAME="${NAME}-Portable" - test -z "${INST}" || mv NMCLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe - test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * - cd .. - done - for d in NMCLauncher-Windows-MinGW-w64*; do cd "${d}" || continue INST="$(echo -n ${d} | grep -o Setup || true)" @@ -96,9 +87,10 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} - name: v${{ env.VERSION }} + name: NMC Launcher ${{ env.VERSION }} draft: true prerelease: false + fail_on_unmatched_files: true files: | NMCLauncher-Linux-x86_64.AppImage NMCLauncher-Linux-x86_64.AppImage.zsync @@ -118,8 +110,5 @@ jobs: NMCLauncher-Windows-MSVC-${{ env.VERSION }}.zip NMCLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip NMCLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe - NMCLauncher-Windows-Clang-${{ env.VERSION }}.zip - NMCLauncher-Windows-Clang-Portable-${{ env.VERSION }}.zip - NMCLauncher-Windows-Clang-Setup-${{ env.VERSION }}.exe NMCLauncher-macOS-${{ env.VERSION }}.zip NMCLauncher-${{ env.VERSION }}.tar.gz diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 106a7844f..b8f8137d7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: days-before-stale: 60 days-before-close: -1 # Don't close anything diff --git a/.gitignore b/.gitignore index 918ed3087..506b02f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt +CMakeUserPresets.json /.project /.settings /.idea @@ -21,12 +22,11 @@ CMakeCache.txt /.vs cmake-build-*/ Debug +compile_commands.json # Build dirs build /build-* -build.bat -.prettierignore # Install dirs install @@ -47,16 +47,11 @@ run/ .cache/ -# Nix/NixOS -.direnv/ -## Used when manually invoking stdenv phases -outputs/ -## Regular artifacts -result -result-* -repl-result-* +# Flatpak +.flatpak-builder +flatbuild # Snap *.snap -cosas/ \ No newline at end of file +/cosas/ diff --git a/.gitmodules b/.gitmodules index 87703fee5..42c566fa8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,21 +1,3 @@ -[submodule "libraries/quazip"] - path = libraries/quazip - url = https://github.com/stachenov/quazip.git -[submodule "libraries/tomlplusplus"] - path = libraries/tomlplusplus - url = https://github.com/marzer/tomlplusplus.git -[submodule "libraries/filesystem"] - path = libraries/filesystem - url = https://github.com/gulrak/filesystem [submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git -[submodule "libraries/zlib"] - path = libraries/zlib - url = https://github.com/madler/zlib.git -[submodule "libraries/extra-cmake-modules"] - path = libraries/extra-cmake-modules - url = https://github.com/KDE/extra-cmake-modules -[submodule "libraries/cmark"] - path = libraries/cmark - url = https://github.com/commonmark/cmark.git diff --git a/.markdownlintignore b/.markdownlintignore index a8669d01d..96f627ad9 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,2 +1 @@ libraries/nbtplusplus -libraries/quazip diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c0ff3fd2..060e34bc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ -cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip +cmake_minimum_required(VERSION 3.22) # minimum version required by Qt -project(Launcher) +project(Launcher LANGUAGES C CXX) +if(APPLE) + enable_language(OBJC OBJCXX) +endif() string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) @@ -24,7 +27,7 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) if(MSVC) @@ -88,44 +91,39 @@ else() endif() endif() -# Fix build with Qt 5.13 -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") - -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") - -# Fix aarch64 build for toml++ -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000") # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") +# Export compile commands for debug builds if we can (useful in LSPs like clangd) +# https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html +if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() + option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser -if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND DEBUG_ADDRESS_SANITIZER) +if (DEBUG_ADDRESS_SANITIZER) message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") - if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") - # using clang with clang-cl front end - message(STATUS "Address Sanitizer available on Clang MSVC frontend") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") - else() - # AppleClang and Clang - message(STATUS "Address Sanitizer available on Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") - endif() - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # GCC - message(STATUS "Address Sanitizer available on GCC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") - link_libraries("asan") - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") - message(STATUS "Address Sanitizer available on MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") + + set(USE_ASAN_COMPILE_OPTIONS $,$>) + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + message(STATUS "Using Address Sanitizer compile options for MSVC frontend") + add_compile_options( + $<${USE_ASAN_COMPILE_OPTIONS}:/fsanitize=address> + $<${USE_ASAN_COMPILE_OPTIONS}:/Oy-> + ) + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + message(STATUS "Using Address Sanitizer compile options for GCC/Clang") + add_compile_options( + $<${USE_ASAN_COMPILE_OPTIONS}:-fsanitize=address,undefined> + $<${USE_ASAN_COMPILE_OPTIONS}:-fno-omit-frame-pointer> + $<${USE_ASAN_COMPILE_OPTIONS}:-fno-sanitize-recover=null> + ) + link_libraries("asan" "ubsan") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() @@ -157,20 +155,9 @@ endif() option(BUILD_TESTING "Build the testing tree." ON) -find_package(ECM QUIET NO_MODULE) -if(NOT ECM_FOUND) - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/CMakeLists.txt") - message(STATUS "Using bundled ECM") - set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/extra-cmake-modules/modules;${CMAKE_MODULE_PATH}") - else() - message(FATAL_ERROR - " Could not find ECM\n \n" - " Either install ECM using the system package manager or clone submodules\n" - " Submodules can be cloned with 'git submodule update --init --recursive'") - endif() -else() - set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") -endif() +find_package(ECM NO_MODULE REQUIRED) +set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") + include(CTest) include(ECMAddTests) if(BUILD_TESTING) @@ -180,19 +167,20 @@ endif() ##################################### Set Application options ##################################### ######## Set URLs ######## -set(Launcher_NEWS_RSS_URL "https://github.com/sogik/NMCLauncher/releases.atom" CACHE STRING "URL to fetch NMC Launcher's releases from GitHub.") +set(Launcher_NEWS_RSS_URL "https://github.com/sogik/NMCLauncher/releases.atom" CACHE STRING "URL to fetch Launcher's news feed from.") set(Launcher_NEWS_OPEN_URL "https://github.com/sogik/NMCLauncher/releases" 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_HELP_URL "https://prismlauncher.org/wiki/overview/troubleshooting/" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 9) -set(Launcher_VERSION_MINOR 5) +set(Launcher_VERSION_MAJOR 10) +set(Launcher_VERSION_MINOR 0) +set(Launcher_VERSION_PATCH 0) -set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") -set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") -set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") +set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") +set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") +set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") @@ -229,9 +217,10 @@ set(Launcher_DISCORD_URL "" CACHE STRING "URL for the Discord guild.") set(Launcher_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.") # Builds -set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") +option(Launcher_USE_PCH "Use precompiled headers where possible" ON) + # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) @@ -239,7 +228,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) - set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader @@ -266,7 +255,7 @@ endif() # By using this key in your builds you accept the terms of use laid down in # https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use -set(Launcher_MSA_CLIENT_ID $ENV{MSA_CLIENT_ID} CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") +set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") # By using this key in your builds you accept the terms and conditions laid down in # https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions @@ -294,75 +283,59 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") ################################ 3rd Party Libs ################################ -# Successive configurations of cmake without cleaning the build dir will cause zlib fallback to fail due to cached values -# Record when fallback triggered and skip this find_package -if(NOT Launcher_FORCE_BUNDLED_LIBS AND NOT FORCE_BUNDLED_ZLIB) - find_package(ZLIB QUIET) -endif() -if(NOT ZLIB_FOUND) - set(FORCE_BUNDLED_ZLIB TRUE CACHE BOOL "") - mark_as_advanced(FORCE_BUNDLED_ZLIB) -endif() - # Find the required Qt parts -include(QtVersionlessBackport) -if(Launcher_QT_VERSION_MAJOR EQUAL 5) - set(QT_VERSION_MAJOR 5) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth) - - if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt5 1.3 QUIET) - endif() - if (NOT QuaZip-Qt5_FOUND) - set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) - set(FORCE_BUNDLED_QUAZIP 1) - endif() - - # Qt 6 sets these by default. Notably causes Windows APIs to use UNICODE strings. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth) - list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) - - if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt6 1.3 QUIET) - endif() - if (NOT QuaZip-Qt6_FOUND) - set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) - set(FORCE_BUNDLED_QUAZIP 1) - endif() + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) + find_package(Qt6 COMPONENTS DBus) + list(APPEND Launcher_QT_DBUS Qt6::DBus) else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() -if(Launcher_QT_VERSION_MAJOR EQUAL 5) - include(ECMQueryQt) - ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) - ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) - ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) -else() +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() -# NOTE: Qt 6 already sets this by default -if (Qt5_POSITION_INDEPENDENT_CODE) - SET(CMAKE_POSITION_INDEPENDENT_CODE ON) +find_package(cmark REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_package(PkgConfig REQUIRED) + pkg_check_modules(gamemode REQUIRED IMPORTED_TARGET gamemode) endif() -if(NOT Launcher_FORCE_BUNDLED_LIBS) - # Find toml++ - find_package(tomlplusplus 3.2.0 QUIET) +# Find libqrencode +## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll +if(MSVC) + find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED) + find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED) + find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded) + set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG}) +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode) +endif() - # Find ghc_filesystem - find_package(ghc_filesystem QUIET) +# Find libarchive through CMake packages, mainly for vcpkg +find_package(LibArchive) +# CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config +if(NOT LibArchive_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) +endif() - # Find cmark - find_package(cmark QUIET) +find_package(tomlplusplus 3.2.0) +# fallback to pkgconfig, important especially as many distros package toml++ built with meson +if(NOT tomlplusplus_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus>=3.2.0) endif() +find_package(ZLIB REQUIRED) + + include(ECMQtDeclareLoggingCategory) ####################################### Program Info ####################################### @@ -376,7 +349,7 @@ set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) - set(Launcher_BUILD_UPDATER YES) + set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) @@ -392,13 +365,10 @@ if(UNIX AND APPLE) set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app") - # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") - set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.prismlauncher.${Launcher_Name}") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "${Launcher_AppID}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") @@ -407,23 +377,63 @@ if(UNIX AND APPLE) set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") - set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive") - set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") + set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive") + set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${MACOSX_SPARKLE_DIR}) - if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") set(Launcher_ENABLE_UPDATER YES) endif() - # install as bundle - set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") - # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) + find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") + if(ACTOOL_EXE) + execute_process( + COMMAND xcodebuild -version + OUTPUT_VARIABLE XCODE_VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + string(REGEX MATCH "Xcode ([0-9]+\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") + if(XCODE_VERSION_MATCH) + set(XCODE_VERSION ${CMAKE_MATCH_1}) + else() + set(XCODE_VERSION 0.0) + endif() + + if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) + set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") + set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") + set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") + + add_custom_command( + OUTPUT "${GENERATED_ASSETS_CAR}" + COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" + --compile "${ASSETS_OUT_DIR}" + --output-partial-info-plist /dev/null + --app-icon NMCLauncher + --enable-on-demand-resources NO + --target-device mac + --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} + --platform macosx + DEPENDS "${ICON_SOURCE}" + COMMENT "Compiling asset catalog (${ICON_SOURCE})" + VERBATIM + ) + add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") + install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") + else() + message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") + endif() + + else() + message(WARNING "actool not found. Cannot compile macOS app icons.\n" + "Install Xcode command line tools: 'xcode-select --install'") + endif() + + elseif(UNIX) include(KDEInstallDirs) @@ -431,30 +441,20 @@ elseif(UNIX) set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(JARS_DEST_DIR "share/${Launcher_Name}") - # install as bundle with no dependencies included - set(INSTALL_BUNDLE "nodeps" CACHE STRING "Use fixup_bundle to bundle dependencies") - # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") - if (INSTALL_BUNDLE STREQUAL full) - set(PLUGIN_DEST_DIR "plugins") - set(BUNDLE_DEST_DIR ".") - set(RESOURCES_DEST_DIR ".") - - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}") - - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) - endif() + set(PLUGIN_DEST_DIR "plugins") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") @@ -470,15 +470,6 @@ elseif(WIN32) set(PLUGIN_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") - - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.exe") - - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) - - # install as bundle - set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") else() message(FATAL_ERROR "Platform not supported") endif() @@ -498,67 +489,10 @@ add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker -if(FORCE_BUNDLED_ZLIB) - message(STATUS "Using bundled zlib") - - set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) # Suppress cmake warnings and allow INTERPROCEDURAL_OPTIMIZATION for zlib - set(SKIP_INSTALL_ALL ON) - add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - - # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. - # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. - check_include_file(unistd.h NEED_GENERATED_ZCONF) - if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) - # zlib's cmake script renames a file, dirtying the submodule, see https://github.com/madler/zlib/issues/162 - message(STATUS "Undoing Rename") - message(STATUS " ${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") - file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h") - endif() - set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" CACHE STRING "" FORCE) - set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") - add_library(ZLIB::ZLIB ALIAS zlibstatic) - set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") - - find_package(ZLIB REQUIRED) -else() - message(STATUS "Using system zlib") -endif() -if (FORCE_BUNDLED_QUAZIP) - message(STATUS "Using bundled QuaZip") - set(BUILD_SHARED_LIBS 0) # link statically to avoid conflicts. - set(QUAZIP_INSTALL 0) - add_subdirectory(libraries/quazip) # zip manipulation library -else() - message(STATUS "Using system QuaZip") -endif() add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions -if(NOT tomlplusplus_FOUND) - message(STATUS "Using bundled tomlplusplus") - add_subdirectory(libraries/tomlplusplus) # toml parser -else() - message(STATUS "Using system tomlplusplus") -endif() -if(NOT cmark_FOUND) - message(STATUS "Using bundled cmark") - set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING}) - set(BUILD_TESTING 0) - set(BUILD_SHARED_LIBS 0) - add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser - add_library(cmark::cmark ALIAS cmark) - set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING}) -else() - message(STATUS "Using system cmark") -endif() -add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API -if (NOT ghc_filesystem_FOUND) - message(STATUS "Using bundled ghc_filesystem") - add_subdirectory(libraries/filesystem) # Implementation of std::filesystem for old C++, for usage in old macOS -else() - message(STATUS "Using system ghc_filesystem") -endif() add_subdirectory(libraries/qdcss) # css parser ############################### Built Artifacts ############################### diff --git a/CMakePresets.json b/CMakePresets.json index f8e688b89..1f28dea27 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,14 +1,256 @@ { - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "cmakeMinimumRequired": { - "major": 3, - "minor": 28 - }, - "include": [ - "cmake/linuxPreset.json", - "cmake/macosPreset.json", - "cmake/windowsMinGWPreset.json", - "cmake/windowsMSVCPreset.json" - ] + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build", + "installDir": "install", + "generator": "Ninja Multi-Config", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", + "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", + "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", + "ENABLE_LTO": "ON" + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "VCPKG_TARGET_TRIPLET": "universal-osx" + } + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_clang", + "displayName": "Windows (Clang)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ], + "buildPresets": [ + { + "name": "linux", + "displayName": "Linux", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "macos", + "displayName": "macOS", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "macos" + ], + "configurePreset": "macos_universal" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_clang", + "displayName": "Windows (Clang)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_clang" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + } + ], + "testPresets": [ + { + "name": "base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "linux", + "displayName": "Linux", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "configurePreset": "linux" + }, + { + "name": "macos", + "displayName": "macOS", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos" + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "configurePreset": "macos_universal" + }, + { + "name": "windows_mingw", + "displayName": "Windows (MinGW)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_mingw" + }, + { + "name": "windows_clang", + "displayName": "Windows (Clang)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_clang" + }, + { + "name": "windows_msvc", + "displayName": "Windows (MSVC)", + "inherits": [ + "base" + ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "configurePreset": "windows_msvc" + } + ] } diff --git a/COPYING.md b/COPYING.md index 810b0a8cc..54778f9c2 100644 --- a/COPYING.md +++ b/COPYING.md @@ -1,7 +1,7 @@ ## NMC Launcher NMC Launcher - Minecraft Launcher - Copyright (C) 2025 sogik + Copyright (C) 2026 sogik This program is free software and is licensed under the BSD 3-Clause License. @@ -30,6 +30,23 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## Prism Launcher + + Prism Launcher - Minecraft Launcher + Copyright (C) 2022-2026 Prism Launcher Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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: diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index e4bb32485..b6683b85b 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -33,9 +33,8 @@ * limitations under the License. */ -#include -#include "BuildConfig.h" #include +#include "BuildConfig.h" const Config BuildConfig; @@ -49,15 +48,15 @@ Config::Config() LAUNCHER_DOMAIN = "@Launcher_Domain@"; LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; LAUNCHER_GIT = "@Launcher_Git@"; - LAUNCHER_DESKTOPFILENAME = "@Launcher_DesktopFileName@"; + LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; USER_AGENT = "@Launcher_UserAgent@"; - USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; + VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; @@ -74,14 +73,13 @@ Config::Config() MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; - if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) - { + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; - } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } - #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER +#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; @@ -89,27 +87,19 @@ Config::Config() GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" - if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") - || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") - || GIT_REFSPEC == QStringLiteral("") - || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) - { + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || + GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } - if (GIT_REFSPEC.startsWith("refs/heads/")) - { + if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; - VERSION_CHANNEL.remove("refs/heads/"); - } - else if (!GIT_COMMIT.isEmpty()) - { + VERSION_CHANNEL.remove("refs/heads/"); + } else if (!GIT_COMMIT.isEmpty()) { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); - } - else - { + } else { VERSION_CHANNEL = "unknown"; } @@ -129,15 +119,15 @@ Config::Config() BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; - THEMES_URL = "@Launcher_THEMES_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; + THEMES_URL = "@Launcher_THEMES_URL@"; } QString Config::versionString() const { - return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR); + return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); } QString Config::printableVersionString() const @@ -145,8 +135,7 @@ QString Config::printableVersionString() const QString vstr = versionString(); // If the build is not a main release, append the channel - if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr) - { + if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { vstr += "-" + VERSION_CHANNEL; } return vstr; @@ -163,4 +152,3 @@ QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } - diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 83f0bbece..52f356821 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -52,13 +52,15 @@ class Config { QString LAUNCHER_DOMAIN; QString LAUNCHER_CONFIGFILE; QString LAUNCHER_GIT; - QString LAUNCHER_DESKTOPFILENAME; + QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; /// The major version number. int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; + /// The patch version number. + int VERSION_PATCH; /** * The version channel @@ -105,9 +107,6 @@ class Config { /// User-Agent to use. QString USER_AGENT; - /// User-Agent to use for uncached requests. - QString USER_AGENT_UNCACHED; - /// The git commit hash of this build QString GIT_COMMIT; @@ -165,10 +164,10 @@ class Config { QString TRANSLATIONS_URL; QString MATRIX_URL; QString DISCORD_URL; - QString THEMES_URL; QString SUBREDDIT_URL; + QString THEMES_URL; - QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; + 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; diff --git a/cmake/ECMQueryQt.cmake b/cmake/ECMQueryQt.cmake deleted file mode 100644 index 98eb50089..000000000 --- a/cmake/ECMQueryQt.cmake +++ /dev/null @@ -1,100 +0,0 @@ -# SPDX-FileCopyrightText: 2014 Rohan Garg -# SPDX-FileCopyrightText: 2014 Alex Merry -# SPDX-FileCopyrightText: 2014-2016 Aleix Pol -# SPDX-FileCopyrightText: 2017 Friedrich W. H. Kossebau -# SPDX-FileCopyrightText: 2022 Ahmad Samir -# -# SPDX-License-Identifier: BSD-3-Clause -#[=======================================================================[.rst: -ECMQueryQt ---------------- -This module can be used to query the installation paths used by Qt. - -For Qt5 this uses ``qmake``, and for Qt6 this used ``qtpaths`` (the latter has built-in -support to query the paths of a target platform when cross-compiling). - -This module defines the following function: -:: - - ecm_query_qt( [TRY]) - -Passing ``TRY`` will result in the method not making the build fail if the executable -used for querying has not been found, but instead simply print a warning message and -return an empty string. - -Example usage: - -.. code-block:: cmake - - include(ECMQueryQt) - ecm_query_qt(bin_dir QT_INSTALL_BINS) - -If the call succeeds ``${bin_dir}`` will be set to ``/path/to/bin/dir`` (e.g. -``/usr/lib64/qt/bin/``). - -Since: 5.93 -#]=======================================================================] - -include(${CMAKE_CURRENT_LIST_DIR}/QtVersionOption.cmake) -include(CheckLanguage) -check_language(CXX) -if (CMAKE_CXX_COMPILER) - # Enable the CXX language to let CMake look for config files in library dirs. - # See: https://gitlab.kitware.com/cmake/cmake/-/issues/23266 - enable_language(CXX) -endif() - -if (QT_MAJOR_VERSION STREQUAL "5") - # QUIET to accommodate the TRY option - find_package(Qt${QT_MAJOR_VERSION}Core QUIET) - if(TARGET Qt5::qmake) - get_target_property(_qmake_executable_default Qt5::qmake LOCATION) - - set(QUERY_EXECUTABLE ${_qmake_executable_default} - CACHE FILEPATH "Location of the Qt5 qmake executable") - set(_exec_name_text "Qt5 qmake") - set(_cli_option "-query") - endif() -elseif(QT_MAJOR_VERSION STREQUAL "6") - # QUIET to accommodate the TRY option - find_package(Qt6 COMPONENTS CoreTools QUIET CONFIG) - if (TARGET Qt6::qtpaths) - get_target_property(_qtpaths_executable Qt6::qtpaths LOCATION) - - set(QUERY_EXECUTABLE ${_qtpaths_executable} - CACHE FILEPATH "Location of the Qt6 qtpaths executable") - set(_exec_name_text "Qt6 qtpaths") - set(_cli_option "--query") - endif() -endif() - -function(ecm_query_qt result_variable qt_variable) - set(options TRY) - set(oneValueArgs) - set(multiValueArgs) - - cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - if(NOT QUERY_EXECUTABLE) - if(ARGS_TRY) - set(${result_variable} "" PARENT_SCOPE) - message(STATUS "No ${_exec_name_text} executable found. Can't check ${qt_variable}") - return() - else() - message(FATAL_ERROR "No ${_exec_name_text} executable found. Can't check ${qt_variable} as required") - endif() - endif() - execute_process( - COMMAND ${QUERY_EXECUTABLE} ${_cli_option} "${qt_variable}" - RESULT_VARIABLE return_code - OUTPUT_VARIABLE output - ) - if(return_code EQUAL 0) - string(STRIP "${output}" output) - file(TO_CMAKE_PATH "${output}" output_path) - set(${result_variable} "${output_path}" PARENT_SCOPE) - else() - message(WARNING "Failed call: ${_command} \"${qt_variable}\"") - message(FATAL_ERROR "${_exec_name_text} call failed: ${return_code}") - endif() -endfunction() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 3a8c8fbfe..0814f7ef0 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -21,7 +21,9 @@ CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} + ${Launcher_Name} + CFBundleIconName + ${Launcher_Name} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion @@ -42,6 +44,8 @@ LSRequiresCarbon + LSApplicationCategoryType + public.app-category.games NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} SUPublicEDKey diff --git a/cmake/QtVersionOption.cmake b/cmake/QtVersionOption.cmake deleted file mode 100644 index 1390f9db6..000000000 --- a/cmake/QtVersionOption.cmake +++ /dev/null @@ -1,38 +0,0 @@ -#.rst: -# QtVersionOption -# --------------- -# -# Adds a build option to select the major Qt version if necessary, -# that is, if the major Qt version has not yet been determined otherwise -# (e.g. by a corresponding find_package() call). -# -# This module is typically included by other modules requiring knowledge -# about the major Qt version. -# -# ``QT_MAJOR_VERSION`` is defined to either be "5" or "6". -# -# -# Since 5.82.0. - -#============================================================================= -# SPDX-FileCopyrightText: 2021 Volker Krause -# -# SPDX-License-Identifier: BSD-3-Clause - -if (DEFINED QT_MAJOR_VERSION) - return() -endif() - -if (TARGET Qt5::Core) - set(QT_MAJOR_VERSION 5) -elseif (TARGET Qt6::Core) - set(QT_MAJOR_VERSION 6) -else() - option(BUILD_WITH_QT6 "Build against Qt 6" OFF) - - if (BUILD_WITH_QT6) - set(QT_MAJOR_VERSION 6) - else() - set(QT_MAJOR_VERSION 5) - endif() -endif() diff --git a/cmake/QtVersionlessBackport.cmake b/cmake/QtVersionlessBackport.cmake deleted file mode 100644 index 46792db58..000000000 --- a/cmake/QtVersionlessBackport.cmake +++ /dev/null @@ -1,97 +0,0 @@ -#============================================================================= -# Copyright 2005-2011 Kitware, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of Kitware, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#============================================================================= - -# From Qt5CoreMacros.cmake - -function(qt_generate_moc) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_generate_moc(${ARGV}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_generate_moc(${ARGV}) - endif() -endfunction() - -function(qt_wrap_cpp outfiles) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_wrap_cpp("${outfiles}" ${ARGN}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_wrap_cpp("${outfiles}" ${ARGN}) - endif() - set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) -endfunction() - -function(qt_add_binary_resources) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_add_binary_resources(${ARGV}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_add_binary_resources(${ARGV}) - endif() -endfunction() - -function(qt_add_resources outfiles) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_add_resources("${outfiles}" ${ARGN}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_add_resources("${outfiles}" ${ARGN}) - endif() - set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) -endfunction() - -function(qt_add_big_resources outfiles) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_add_big_resources(${outfiles} ${ARGN}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_add_big_resources(${outfiles} ${ARGN}) - endif() - set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) -endfunction() - -function(qt_import_plugins) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_import_plugins(${ARGV}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_import_plugins(${ARGV}) - endif() -endfunction() - - -# From Qt5WidgetsMacros.cmake - -function(qt_wrap_ui outfiles) - if(QT_VERSION_MAJOR EQUAL 5) - qt5_wrap_ui("${outfiles}" ${ARGN}) - elseif(QT_VERSION_MAJOR EQUAL 6) - qt6_wrap_ui("${outfiles}" ${ARGN}) - endif() - set("${outfiles}" "${${outfiles}}" PARENT_SCOPE) -endfunction() - diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json deleted file mode 100644 index 6f288c7de..000000000 --- a/cmake/commonPresets.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "configurePresets": [ - { - "name": "base", - "hidden": true, - "binaryDir": "build", - "installDir": "install", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", - "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}" - } - }, - { - "name": "base_debug", - "hidden": true, - "inherits": [ - "base" - ], - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "base_release", - "hidden": true, - "inherits": [ - "base" - ], - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "ENABLE_LTO": "ON" - } - }, - { - "name": "base_ci", - "hidden": true, - "inherits": [ - "base_release" - ], - "cacheVariables": { - "Launcher_FORCE_BUNDLED_LIBS": "ON" - } - } - ], - "testPresets": [ - { - "name": "base", - "hidden": true, - "output": { - "outputOnFailure": true - }, - "execution": { - "noTestsAction": "error" - }, - "filter": { - "exclude": { - "name": "^example64|example$" - } - } - }, - { - "name": "base_debug", - "hidden": true, - "inherits": [ - "base" - ], - "output": { - "debug": true - } - }, - { - "name": "base_release", - "hidden": true, - "inherits": [ - "base" - ] - } - ] -} diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json deleted file mode 100644 index 984defa5d..000000000 --- a/cmake/linuxPreset.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "include": [ - "commonPresets.json" - ], - "configurePresets": [ - { - "name": "linux_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - }, - "generator": "Ninja", - "cacheVariables": { - "Launcher_ENABLE_JAVA_DOWNLOADER": "ON" - } - }, - { - "name": "linux_debug", - "inherits": [ - "base_debug", - "linux_base" - ], - "displayName": "Linux (Debug)" - }, - { - "name": "linux_release", - "inherits": [ - "base_release", - "linux_base" - ], - "displayName": "Linux (Release)" - }, - { - "name": "linux_ci", - "inherits": [ - "base_ci", - "linux_base" - ], - "displayName": "Linux (CI)", - "installDir": "/usr" - } - ], - "buildPresets": [ - { - "name": "linux_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - } - }, - { - "name": "linux_debug", - "inherits": [ - "linux_base" - ], - "displayName": "Linux (Debug)", - "configurePreset": "linux_debug" - }, - { - "name": "linux_release", - "inherits": [ - "linux_base" - ], - "displayName": "Linux (Release)", - "configurePreset": "linux_release" - }, - { - "name": "linux_ci", - "inherits": [ - "linux_base" - ], - "displayName": "Linux (CI)", - "configurePreset": "linux_ci" - } - ], - "testPresets": [ - { - "name": "linux_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - } - }, - { - "name": "linux_debug", - "inherits": [ - "base_debug", - "linux_base" - ], - "displayName": "Linux (Debug)", - "configurePreset": "linux_debug" - }, - { - "name": "linux_release", - "inherits": [ - "base_release", - "linux_base" - ], - "displayName": "Linux (Release)", - "configurePreset": "linux_release" - }, - { - "name": "linux_ci", - "inherits": [ - "base_release", - "linux_base" - ], - "displayName": "Linux (CI)", - "configurePreset": "linux_ci" - } - ], - "workflowPresets": [ - { - "name": "linux_debug", - "displayName": "Linux (Debug)", - "steps": [ - { - "type": "configure", - "name": "linux_debug" - }, - { - "type": "build", - "name": "linux_debug" - }, - { - "type": "test", - "name": "linux_debug" - } - ] - }, - { - "name": "linux", - "displayName": "Linux (Release)", - "steps": [ - { - "type": "configure", - "name": "linux_release" - }, - { - "type": "build", - "name": "linux_release" - }, - { - "type": "test", - "name": "linux_release" - } - ] - }, - { - "name": "linux_ci", - "displayName": "Linux (CI)", - "steps": [ - { - "type": "configure", - "name": "linux_ci" - }, - { - "type": "build", - "name": "linux_ci" - }, - { - "type": "test", - "name": "linux_ci" - } - ] - } - ] -} diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json deleted file mode 100644 index de503d7a2..000000000 --- a/cmake/macosPreset.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "include": [ - "commonPresets.json" - ], - "configurePresets": [ - { - "name": "macos_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - }, - "generator": "Ninja" - }, - { - "name": "macos_universal_base", - "hidden": true, - "inherits": [ - "macos_base" - ], - "cacheVariables": { - "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64" - } - }, - { - "name": "macos_debug", - "inherits": [ - "base_debug", - "macos_base" - ], - "displayName": "macOS (Debug)" - }, - { - "name": "macos_release", - "inherits": [ - "base_release", - "macos_base" - ], - "displayName": "macOS (Release)" - }, - { - "name": "macos_universal_debug", - "inherits": [ - "base_debug", - "macos_universal_base" - ], - "displayName": "macOS (Universal Binary, Debug)" - }, - { - "name": "macos_universal_release", - "inherits": [ - "base_release", - "macos_universal_base" - ], - "displayName": "macOS (Universal Binary, Release)" - }, - { - "name": "macos_ci", - "inherits": [ - "base_ci", - "macos_universal_base" - ], - "displayName": "macOS (CI)" - } - ], - "buildPresets": [ - { - "name": "macos_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - } - }, - { - "name": "macos_debug", - "inherits": [ - "macos_base" - ], - "displayName": "macOS (Debug)", - "configurePreset": "macos_debug" - }, - { - "name": "macos_release", - "inherits": [ - "macos_base" - ], - "displayName": "macOS (Release)", - "configurePreset": "macos_release" - }, - { - "name": "macos_universal_debug", - "inherits": [ - "macos_base" - ], - "displayName": "macOS (Universal Binary, Debug)", - "configurePreset": "macos_universal_debug" - }, - { - "name": "macos_universal_release", - "inherits": [ - "macos_base" - ], - "displayName": "macOS (Universal Binary, Release)", - "configurePreset": "macos_universal_release" - }, - { - "name": "macos_ci", - "inherits": [ - "macos_base" - ], - "displayName": "macOS (CI)", - "configurePreset": "macos_ci" - } - ], - "testPresets": [ - { - "name": "macos_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Darwin" - } - }, - { - "name": "macos_debug", - "inherits": [ - "base_debug", - "macos_base" - ], - "displayName": "MacOS (Debug)", - "configurePreset": "macos_debug" - }, - { - "name": "macos_release", - "inherits": [ - "base_release", - "macos_base" - ], - "displayName": "macOS (Release)", - "configurePreset": "macos_release" - }, - { - "name": "macos_universal_debug", - "inherits": [ - "base_debug", - "macos_base" - ], - "displayName": "MacOS (Universal Binary, Debug)", - "configurePreset": "macos_universal_debug" - }, - { - "name": "macos_universal_release", - "inherits": [ - "base_release", - "macos_base" - ], - "displayName": "macOS (Universal Binary, Release)", - "configurePreset": "macos_universal_release" - }, - { - "name": "macos_ci", - "inherits": [ - "base_release", - "macos_base" - ], - "displayName": "macOS (CI)", - "configurePreset": "macos_ci" - } - ], - "workflowPresets": [ - { - "name": "macos_debug", - "displayName": "macOS (Debug)", - "steps": [ - { - "type": "configure", - "name": "macos_debug" - }, - { - "type": "build", - "name": "macos_debug" - }, - { - "type": "test", - "name": "macos_debug" - } - ] - }, - { - "name": "macos", - "displayName": "macOS (Release)", - "steps": [ - { - "type": "configure", - "name": "macos_release" - }, - { - "type": "build", - "name": "macos_release" - }, - { - "type": "test", - "name": "macos_release" - } - ] - }, - { - "name": "macos_universal_debug", - "displayName": "macOS (Universal Binary, Debug)", - "steps": [ - { - "type": "configure", - "name": "macos_universal_debug" - }, - { - "type": "build", - "name": "macos_universal_debug" - }, - { - "type": "test", - "name": "macos_universal_debug" - } - ] - }, - { - "name": "macos_universal", - "displayName": "macOS (Universal Binary, Release)", - "steps": [ - { - "type": "configure", - "name": "macos_universal_release" - }, - { - "type": "build", - "name": "macos_universal_release" - }, - { - "type": "test", - "name": "macos_universal_release" - } - ] - }, - { - "name": "macos_ci", - "displayName": "macOS (CI)", - "steps": [ - { - "type": "configure", - "name": "macos_ci" - }, - { - "type": "build", - "name": "macos_ci" - }, - { - "type": "test", - "name": "macos_ci" - } - ] - } - ] -} diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/README.md b/cmake/vcpkg-ports/vcpkg-tool-meson/README.md new file mode 100644 index 000000000..9047c8037 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/README.md @@ -0,0 +1,3 @@ +The only difference between this and the upstream vcpkg port is the addition of `universal-osx.patch`. It's very annoying we need to bundle this entire tree to do that. + +-@getchoo diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch new file mode 100644 index 000000000..ad800aa66 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py +index 11a00be5d..89ae490ff 100644 +--- a/mesonbuild/cmake/toolchain.py ++++ b/mesonbuild/cmake/toolchain.py +@@ -202,7 +202,7 @@ class CMakeToolchain: + @staticmethod + def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: + if compiler.get_argument_syntax() == 'msvc': +- return arg.startswith('/') ++ return arg.startswith(('/','-')) + else: + if os.path.basename(compiler.get_exe()) == 'zig' and arg in {'ar', 'cc', 'c++', 'dlltool', 'lib', 'ranlib', 'objcopy', 'rc'}: + return True diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch new file mode 100644 index 000000000..0cbfe717d --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch @@ -0,0 +1,45 @@ +diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py +index 883a29a..d9a82af 100644 +--- a/mesonbuild/dependencies/python.py ++++ b/mesonbuild/dependencies/python.py +@@ -232,8 +232,10 @@ class _PythonDependencyBase(_Base): + else: + if self.is_freethreaded: + libpath = Path('libs') / f'python{vernum}t.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}t.lib' + else: + libpath = Path('libs') / f'python{vernum}.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}.lib' + # For a debug build, pyconfig.h may force linking with + # pythonX_d.lib (see meson#10776). This cannot be avoided + # and won't work unless we also have a debug build of +@@ -250,6 +252,8 @@ class _PythonDependencyBase(_Base): + vscrt = self.env.coredata.optstore.get_value('b_vscrt') + if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: + vscrt_debug = True ++ if is_debug_build: ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'debug/lib' / f'python{vernum}_d.lib' + if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): + mlog.warning(textwrap.dedent('''\ + Using a debug build type with MSVC or an MSVC-compatible compiler +@@ -350,9 +354,10 @@ class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + self.is_found = True + + # compile args ++ verdot = self.variables.get('py_version_short') + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), +- self.paths.get('include'), ++ self.paths.get('include') + f'/../../../include/python${verdot}', + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] +@@ -416,7 +421,7 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', + candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) + # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, + # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. +- if pkg_libdir is not None: ++ if True or pkg_libdir is not None: + candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) + else: + candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch new file mode 100644 index 000000000..394b064dc --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch @@ -0,0 +1,52 @@ +From a16ec8b0fb6d7035b669a13edd4d97ff0c307a0b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Martin=20D=C3=B8rum?= +Date: Fri, 2 May 2025 10:56:28 +0200 +Subject: [PATCH] cpp: fix _LIBCPP_ENABLE_ASSERTIONS warning + +libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS from version 18. +However, the libc++ shipped with Apple Clang backported that +deprecation in version 17 already, +which is the version which Apple currently ships for macOS. +This PR changes the _LIBCPP_ENABLE_ASSERTIONS deprecation check +to use version ">=17" on Apple Clang. +--- + mesonbuild/compilers/cpp.py | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py +index 01b9bb9fa34f..f7dc150e8608 100644 +--- a/mesonbuild/compilers/cpp.py ++++ b/mesonbuild/compilers/cpp.py +@@ -311,6 +311,9 @@ def get_option_link_args(self, target: 'BuildTarget', env: 'Environment', subpro + return libs + return [] + ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ return version_compare(self.version, ">=18") ++ + def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if disable: + return ['-DNDEBUG'] +@@ -323,7 +326,7 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if self.language_stdlib_provider(env) == 'stdc++': + return ['-D_GLIBCXX_ASSERTIONS=1'] + else: +- if version_compare(self.version, '>=18'): ++ if self.is_libcpp_enable_assertions_deprecated(): + return ['-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST'] + elif version_compare(self.version, '>=15'): + return ['-D_LIBCPP_ENABLE_ASSERTIONS=1'] +@@ -343,7 +346,12 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): + + + class AppleClangCPPCompiler(AppleCompilerMixin, AppleCPPStdsMixin, ClangCPPCompiler): +- pass ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ # Upstream libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS ++ # in favor of _LIBCPP_HARDENING_MODE from version 18 onwards, ++ # but Apple Clang 17's libc++ has back-ported that change. ++ # See: https://github.com/mesonbuild/meson/issues/14440 ++ return version_compare(self.version, ">=17") + + + class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake new file mode 100644 index 000000000..84201aa1a --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake @@ -0,0 +1,5 @@ +file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/meson") +file(INSTALL "${SOURCE_PATH}/meson.py" + "${SOURCE_PATH}/mesonbuild" + DESTINATION "${CURRENT_PACKAGES_DIR}/tools/meson" +) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch new file mode 100644 index 000000000..8f2a029de --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py +--- a/mesonbuild/dependencies/misc.py ++++ b/mesonbuild/dependencies/misc.py +@@ -593,7 +593,8 @@ iconv_factory = DependencyFactory( + + packages['intl'] = intl_factory = DependencyFactory( + 'intl', ++ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], ++ cmake_name='Intl', +- [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], + builtin_class=IntlBuiltinDependency, + system_class=IntlSystemDependency, + ) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in b/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in new file mode 100644 index 000000000..df21b753b --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in @@ -0,0 +1,43 @@ +[binaries] +cmake = ['@CMAKE_COMMAND@'] +ninja = ['@NINJA@'] +pkg-config = ['@PKGCONFIG@'] +@MESON_MT@ +@MESON_AR@ +@MESON_RC@ +@MESON_C@ +@MESON_C_LD@ +@MESON_CXX@ +@MESON_CXX_LD@ +@MESON_OBJC@ +@MESON_OBJC_LD@ +@MESON_OBJCPP@ +@MESON_OBJCPP_LD@ +@MESON_FC@ +@MESON_FC_LD@ +@MESON_WINDRES@ +@MESON_ADDITIONAL_BINARIES@ +[properties] +cmake_toolchain_file = '@SCRIPTS@/buildsystems/vcpkg.cmake' +@MESON_ADDITIONAL_PROPERTIES@ +[cmake] +CMAKE_BUILD_TYPE = '@MESON_CMAKE_BUILD_TYPE@' +VCPKG_TARGET_TRIPLET = '@TARGET_TRIPLET@' +VCPKG_HOST_TRIPLET = '@_HOST_TRIPLET@' +VCPKG_CHAINLOAD_TOOLCHAIN_FILE = '@VCPKG_CHAINLOAD_TOOLCHAIN_FILE@' +VCPKG_CRT_LINKAGE = '@VCPKG_CRT_LINKAGE@' +_VCPKG_INSTALLED_DIR = '@_VCPKG_INSTALLED_DIR@' +@MESON_HOST_MACHINE@ +@MESON_BUILD_MACHINE@ +[built-in options] +default_library = '@MESON_DEFAULT_LIBRARY@' +werror = false +@MESON_CFLAGS@ +@MESON_CXXFLAGS@ +@MESON_FCFLAGS@ +@MESON_OBJCFLAGS@ +@MESON_OBJCPPFLAGS@ +# b_vscrt +@MESON_VSCRT_LINKAGE@ +# c_winlibs/cpp_winlibs +@MESON_WINLIBS@ \ No newline at end of file diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake new file mode 100644 index 000000000..fdea886a7 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake @@ -0,0 +1,45 @@ +# This port represents a dependency on the Meson build system. +# In the future, it is expected that this port acquires and installs Meson. +# Currently is used in ports that call vcpkg_find_acquire_program(MESON) in order to force rebuilds. + +set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled) + +set(patches + meson-intl.patch + adjust-python-dep.patch + adjust-args.patch + remove-freebsd-pcfile-specialization.patch + fix-libcpp-enable-assertions.patch # https://github.com/mesonbuild/meson/pull/14548, Remove in 1.8.3 + universal-osx.patch # NOTE(@getchoo): THIS IS THE ONLY CHANGE NEEDED FOR PRISM +) +set(scripts + vcpkg-port-config.cmake + vcpkg_configure_meson.cmake + vcpkg_install_meson.cmake + meson.template.in +) +set(to_hash + "${CMAKE_CURRENT_LIST_DIR}/vcpkg.json" + "${CMAKE_CURRENT_LIST_DIR}/portfile.cmake" +) +foreach(file IN LISTS patches scripts) + set(filepath "${CMAKE_CURRENT_LIST_DIR}/${file}") + list(APPEND to_hash "${filepath}") + file(COPY "${filepath}" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +endforeach() + +set(meson_path_hash "") +foreach(filepath IN LISTS to_hash) + file(SHA1 "${filepath}" to_append) + string(APPEND meson_path_hash "${to_append}") +endforeach() +string(SHA512 meson_path_hash "${meson_path_hash}") + +string(SUBSTRING "${meson_path_hash}" 0 6 MESON_SHORT_HASH) +list(TRANSFORM patches REPLACE [[^(..*)$]] [["${CMAKE_CURRENT_LIST_DIR}/\0"]]) +list(JOIN patches "\n " PATCHES) +configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY) + +vcpkg_install_copyright(FILE_LIST "${VCPKG_ROOT_DIR}/LICENSE.txt") + +include("${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch new file mode 100644 index 000000000..947345ccf --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch @@ -0,0 +1,23 @@ +diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py +index cc0450a52..13501466d 100644 +--- a/mesonbuild/modules/pkgconfig.py ++++ b/mesonbuild/modules/pkgconfig.py +@@ -701,16 +701,8 @@ class PkgConfigModule(NewExtensionModule): + pcfile = filebase + '.pc' + pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir + if pkgroot is None: +- m = state.environment.machines.host +- if m.is_freebsd(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'libdata', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') +- elif m.is_haiku(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') +- else: +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') +- pkgroot_name = os.path.join('{libdir}', 'pkgconfig') ++ pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') ++ pkgroot_name = os.path.join('{libdir}', 'pkgconfig') + relocatable = state.get_option('pkgconfig.relocatable') + self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, + version, pcfile, conflicts, variables, diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch new file mode 100644 index 000000000..58b96d5ce --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch @@ -0,0 +1,16 @@ +diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py +index f57957f0b..a72e72a0b 100644 +--- a/mesonbuild/compilers/detect.py ++++ b/mesonbuild/compilers/detect.py +@@ -1472,6 +1472,11 @@ def _get_clang_compiler_defines(compiler: T.List[str], lang: str) -> T.Dict[str, + """ + from .mixins.clang import clang_lang_map + ++ # Filter out `-arch` flags passed to the compiler for Universal Binaries ++ # https://github.com/mesonbuild/meson/issues/5290 ++ # https://github.com/mesonbuild/meson/issues/8206 ++ compiler = [arg for i, arg in enumerate(compiler) if not (i > 0 and compiler[i - 1] == "-arch") and not arg == "-arch"] ++ + def _try_obtain_compiler_defines(args: T.List[str]) -> str: + mlog.debug(f'Running command: {join_args(args)}') + p, output, error = Popen_safe(compiler + args, write='', stdin=subprocess.PIPE) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake new file mode 100644 index 000000000..c0dee3a38 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake @@ -0,0 +1,62 @@ +include("${CURRENT_HOST_INSTALLED_DIR}/share/vcpkg-cmake-get-vars/vcpkg-port-config.cmake") +# Overwrite builtin scripts +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_configure_meson.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_install_meson.cmake") + +set(meson_short_hash @MESON_SHORT_HASH@) + +# Setup meson: +set(program MESON) +set(program_version @VERSION@) +set(program_name meson) +set(search_names meson meson.py) +set(ref "${program_version}") +set(path_to_search "${DOWNLOADS}/tools/meson-${program_version}-${meson_short_hash}") +set(download_urls "https://github.com/mesonbuild/meson/archive/${ref}.tar.gz") +set(download_filename "meson-${ref}.tar.gz") +set(download_sha512 bd2e65f0863d9cb974e659ff502d773e937b8a60aaddfd7d81e34cd2c296c8e82bf214d790ac089ba441543059dfc2677ba95ed51f676df9da420859f404a907) + +find_program(SCRIPT_MESON NAMES ${search_names} PATHS "${path_to_search}" NO_DEFAULT_PATH) # NO_DEFAULT_PATH due top patching + +if(NOT SCRIPT_MESON) + vcpkg_download_distfile(archive_path + URLS ${download_urls} + SHA512 "${download_sha512}" + FILENAME "${download_filename}" + ) + file(REMOVE_RECURSE "${path_to_search}") + file(REMOVE_RECURSE "${path_to_search}-tmp") + file(MAKE_DIRECTORY "${path_to_search}-tmp") + file(ARCHIVE_EXTRACT INPUT "${archive_path}" + DESTINATION "${path_to_search}-tmp" + #PATTERNS "**/mesonbuild/*" "**/*.py" + ) + z_vcpkg_apply_patches( + SOURCE_PATH "${path_to_search}-tmp/meson-${ref}" + PATCHES + @PATCHES@ + ) + file(MAKE_DIRECTORY "${path_to_search}") + file(RENAME "${path_to_search}-tmp/meson-${ref}/meson.py" "${path_to_search}/meson.py") + file(RENAME "${path_to_search}-tmp/meson-${ref}/mesonbuild" "${path_to_search}/mesonbuild") + file(REMOVE_RECURSE "${path_to_search}-tmp") + set(SCRIPT_MESON "${path_to_search}/meson.py") +endif() + +# Check required python version +vcpkg_find_acquire_program(PYTHON3) +vcpkg_execute_in_download_mode( + COMMAND "${PYTHON3}" --version + OUTPUT_VARIABLE version_contents + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" +) +string(REGEX MATCH [[[0-9]+\.[0-9]+\.[0-9]+]] python_ver "${version_contents}") + +set(min_required 3.7) +if(python_ver VERSION_LESS "${min_required}") + message(FATAL_ERROR "Found Python version '${python_ver} at ${PYTHON3}' is insufficient for meson. meson requires at least version '${min_required}'") +else() + message(STATUS "Found Python version '${python_ver} at ${PYTHON3}'") +endif() + +message(STATUS "Using meson: ${SCRIPT_MESON}") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json new file mode 100644 index 000000000..04a0cbbec --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json @@ -0,0 +1,11 @@ +{ + "name": "vcpkg-tool-meson", + "version": "1.8.2", + "description": "Meson build system", + "homepage": "https://github.com/mesonbuild/meson", + "license": "Apache-2.0", + "supports": "native", + "dependencies": [ + "vcpkg-cmake-get-vars" + ] +} diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake new file mode 100644 index 000000000..6b00200d1 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake @@ -0,0 +1,480 @@ +function(z_vcpkg_meson_set_proglist_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS) + set(proglist MT AR) + else() + set(proglist AR RANLIB STRIP NM OBJDUMP DLLTOOL MT) + endif() + foreach(prog IN LISTS proglist) + if(VCPKG_DETECTED_CMAKE_${prog}) + if(meson_${prog}) + string(TOUPPER "MESON_${meson_${prog}}" var_to_set) + set("${var_to_set}" "${meson_${prog}} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + elseif(${prog} STREQUAL AR AND VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}) + # Probably need to move AR somewhere else + string(TOLOWER "${prog}" proglower) + z_vcpkg_meson_convert_compiler_flags_to_list(ar_flags "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}}") + list(PREPEND ar_flags "${VCPKG_DETECTED_CMAKE_${prog}}") + z_vcpkg_meson_convert_list_to_python_array(ar_flags ${ar_flags}) + set("MESON_AR" "${proglower} = ${ar_flags}" PARENT_SCOPE) + else() + string(TOUPPER "MESON_${prog}" var_to_set) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + endif() + endif() + endforeach() + set(compilers "${arg_LANGUAGES}") + if(VCPKG_TARGET_IS_WINDOWS) + list(APPEND compilers RC) + endif() + set(meson_RC windres) + set(meson_Fortran fortran) + set(meson_CXX cpp) + foreach(prog IN LISTS compilers) + if(VCPKG_DETECTED_CMAKE_${prog}_COMPILER) + string(TOUPPER "MESON_${prog}" var_to_set) + if(meson_${prog}) + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + set("${var_to_set}" "${meson_${prog}} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${meson_${prog}}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + else() + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${proglower}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + endif() + endif() + endforeach() +endfunction() + +function(z_vcpkg_meson_convert_compiler_flags_to_list out_var compiler_flags) + separate_arguments(cmake_list NATIVE_COMMAND "${compiler_flags}") + list(TRANSFORM cmake_list REPLACE ";" [[\\;]]) + set("${out_var}" "${cmake_list}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_convert_list_to_python_array out_var) + z_vcpkg_function_arguments(flag_list 1) + vcpkg_list(REMOVE_ITEM flag_list "") # remove empty elements if any + vcpkg_list(JOIN flag_list "', '" flag_list) + set("${out_var}" "['${flag_list}']" PARENT_SCOPE) +endfunction() + +# Generates the required compiler properties for meson +function(z_vcpkg_meson_set_flags_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + set(libpath_flag /LIBPATH:) + else() + set(libpath_flag -L) + endif() + if(config_type STREQUAL "DEBUG") + set(path_suffix "/debug") + else() + set(path_suffix "") + endif() + + set(includepath "-I${CURRENT_INSTALLED_DIR}/include") + set(libpath "${libpath_flag}${CURRENT_INSTALLED_DIR}${path_suffix}/lib") + + foreach(lang IN LISTS arg_LANGUAGES) + z_vcpkg_meson_convert_compiler_flags_to_list(${lang}flags "${VCPKG_COMBINED_${lang}_FLAGS_${config_type}}") + if(lang MATCHES "^(C|CXX)$") + vcpkg_list(APPEND ${lang}flags "${includepath}") + endif() + z_vcpkg_meson_convert_list_to_python_array(${lang}flags ${${lang}flags}) + set(lang_mapping "${lang}") + if(lang STREQUAL "Fortran") + set(lang_mapping "FC") + endif() + string(TOLOWER "${lang_mapping}" langlower) + if(lang STREQUAL "CXX") + set(langlower cpp) + endif() + set(MESON_${lang_mapping}FLAGS "${langlower}_args = ${${lang}flags}\n") + set(linker_flags "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_${config_type}}") + z_vcpkg_meson_convert_compiler_flags_to_list(linker_flags "${linker_flags}") + vcpkg_list(APPEND linker_flags "${libpath}") + z_vcpkg_meson_convert_list_to_python_array(linker_flags ${linker_flags}) + string(APPEND MESON_${lang_mapping}FLAGS "${langlower}_link_args = ${linker_flags}\n") + set(MESON_${lang_mapping}FLAGS "${MESON_${lang_mapping}FLAGS}" PARENT_SCOPE) + endforeach() +endfunction() + +function(z_vcpkg_get_build_and_host_system build_system host_system is_cross) #https://mesonbuild.com/Cross-compilation.html + set(build_unknown FALSE) + if(CMAKE_HOST_WIN32) + if(DEFINED ENV{PROCESSOR_ARCHITEW6432}) + set(build_arch $ENV{PROCESSOR_ARCHITEW6432}) + else() + set(build_arch $ENV{PROCESSOR_ARCHITECTURE}) + endif() + if(build_arch MATCHES "(amd|AMD)64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(build_arch MATCHES "(x|X)86") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(build_arch MATCHES "^(ARM|arm)64$") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(build_arch MATCHES "^(ARM|arm)$") + set(build_cpu_fam arm) + set(build_cpu armv7hl) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported build architecture ${build_arch}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + elseif(CMAKE_HOST_UNIX) + # at this stage, CMAKE_HOST_SYSTEM_PROCESSOR is not defined + execute_process( + COMMAND uname -m + OUTPUT_VARIABLE MACHINE + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + + # Show real machine architecture to visually understand whether we are in a native Apple Silicon terminal or running under Rosetta emulation + debug_message("Machine: ${MACHINE}") + + if(MACHINE MATCHES "arm64|aarch64") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(MACHINE MATCHES "armv7h?l") + set(build_cpu_fam arm) + set(build_cpu ${MACHINE}) + elseif(MACHINE MATCHES "x86_64|amd64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(MACHINE MATCHES "x86|i686") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(MACHINE MATCHES "i386") + set(build_cpu_fam x86) + set(build_cpu i386) + elseif(MACHINE MATCHES "loongarch64") + set(build_cpu_fam loongarch64) + set(build_cpu loongarch64) + else() + # https://github.com/mesonbuild/meson/blob/master/docs/markdown/Reference-tables.md#cpu-families + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unhandled machine: ${MACHINE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Failed to detect the build architecture! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + + set(build "[build_machine]\n") # Machine the build is performed on + string(APPEND build "endian = 'little'\n") + if(CMAKE_HOST_WIN32) + string(APPEND build "system = 'windows'\n") + elseif(CMAKE_HOST_APPLE) + string(APPEND build "system = 'darwin'\n") + elseif(CYGWIN) + string(APPEND build "system = 'cygwin'\n") + elseif(CMAKE_HOST_UNIX) + string(APPEND build "system = 'linux'\n") + else() + set(build_unknown TRUE) + endif() + + if(DEFINED build_cpu_fam) + string(APPEND build "cpu_family = '${build_cpu_fam}'\n") + endif() + if(DEFINED build_cpu) + string(APPEND build "cpu = '${build_cpu}'") + endif() + if(NOT build_unknown) + set(${build_system} "${build}" PARENT_SCOPE) + endif() + + set(host_unkown FALSE) + if(VCPKG_TARGET_ARCHITECTURE MATCHES "(amd|AMD|x|X)64") + set(host_cpu_fam x86_64) + set(host_cpu x86_64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "(x|X)86") + set(host_cpu_fam x86) + set(host_cpu i686) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") + set(host_cpu_fam aarch64) + set(host_cpu armv8) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)$") + set(host_cpu_fam arm) + set(host_cpu armv7hl) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "loongarch64") + set(host_cpu_fam loongarch64) + set(host_cpu loongarch64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "wasm32") + set(host_cpu_fam wasm32) + set(host_cpu wasm32) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported target architecture ${VCPKG_TARGET_ARCHITECTURE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the host_machine entry!" ) + endif() + set(host_unkown TRUE) + endif() + + set(host "[host_machine]\n") # host=target in vcpkg. + string(APPEND host "endian = 'little'\n") + if(NOT VCPKG_CMAKE_SYSTEM_NAME OR VCPKG_TARGET_IS_MINGW OR VCPKG_TARGET_IS_UWP) + set(meson_system_name "windows") + else() + string(TOLOWER "${VCPKG_CMAKE_SYSTEM_NAME}" meson_system_name) + endif() + string(APPEND host "system = '${meson_system_name}'\n") + string(APPEND host "cpu_family = '${host_cpu_fam}'\n") + string(APPEND host "cpu = '${host_cpu}'") + if(NOT host_unkown) + set(${host_system} "${host}" PARENT_SCOPE) + endif() + + if(NOT build_cpu_fam MATCHES "${host_cpu_fam}" + OR VCPKG_TARGET_IS_ANDROID OR VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_UWP + OR (VCPKG_TARGET_IS_MINGW AND NOT CMAKE_HOST_WIN32)) + set(${is_cross} TRUE PARENT_SCOPE) + endif() +endfunction() + +function(z_vcpkg_meson_setup_extra_windows_variables config_type) + ## b_vscrt + if(VCPKG_CRT_LINKAGE STREQUAL "static") + set(crt_type "mt") + else() + set(crt_type "md") + endif() + if(config_type STREQUAL "DEBUG") + set(crt_type "${crt_type}d") + endif() + set(MESON_VSCRT_LINKAGE "b_vscrt = '${crt_type}'" PARENT_SCOPE) + ## winlibs + separate_arguments(c_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_C_STANDARD_LIBRARIES}") + separate_arguments(cpp_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_CXX_STANDARD_LIBRARIES}") + z_vcpkg_meson_convert_list_to_python_array(c_winlibs ${c_winlibs}) + z_vcpkg_meson_convert_list_to_python_array(cpp_winlibs ${cpp_winlibs}) + set(MESON_WINLIBS "c_winlibs = ${c_winlibs}\n") + string(APPEND MESON_WINLIBS "cpp_winlibs = ${cpp_winlibs}") + set(MESON_WINLIBS "${MESON_WINLIBS}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_setup_variables config_type) + set(meson_var_list VSCRT_LINKAGE WINLIBS MT AR RC C C_LD CXX CXX_LD OBJC OBJC_LD OBJCXX OBJCXX_LD FC FC_LD WINDRES CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS FCFLAGS SHARED_LINKER_FLAGS) + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "") + endforeach() + + if(VCPKG_TARGET_IS_WINDOWS) + z_vcpkg_meson_setup_extra_windows_variables("${config_type}") + endif() + + z_vcpkg_meson_set_proglist_variables("${config_type}") + z_vcpkg_meson_set_flags_variables("${config_type}") + + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "${MESON_${var}}" PARENT_SCOPE) + endforeach() +endfunction() + +function(vcpkg_generate_meson_cmd_args) + cmake_parse_arguments(PARSE_ARGV 0 arg + "" + "OUTPUT;CONFIG" + "OPTIONS;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(NOT arg_LANGUAGES) + set(arg_LANGUAGES C CXX) + endif() + + vcpkg_list(JOIN arg_ADDITIONAL_BINARIES "\n" MESON_ADDITIONAL_BINARIES) + vcpkg_list(JOIN arg_ADDITIONAL_PROPERTIES "\n" MESON_ADDITIONAL_PROPERTIES) + + set(buildtype "${arg_CONFIG}") + + if(NOT VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + z_vcpkg_select_default_vcpkg_chainload_toolchain() + endif() + vcpkg_list(APPEND VCPKG_CMAKE_CONFIGURE_OPTIONS "-DVCPKG_LANGUAGES=${arg_LANGUAGES}") + vcpkg_cmake_get_vars(cmake_vars_file) + debug_message("Including cmake vars from: ${cmake_vars_file}") + include("${cmake_vars_file}") + + vcpkg_list(APPEND arg_OPTIONS --backend ninja --wrap-mode nodownload -Doptimization=plain) + + z_vcpkg_get_build_and_host_system(MESON_HOST_MACHINE MESON_BUILD_MACHINE IS_CROSS) + + if(arg_CONFIG STREQUAL "DEBUG") + set(suffix "dbg") + else() + string(SUBSTRING "${arg_CONFIG}" 0 3 suffix) + string(TOLOWER "${suffix}" suffix) + endif() + set(meson_input_file_${buildtype} "${CURRENT_BUILDTREES_DIR}/meson-${TARGET_TRIPLET}-${suffix}.log") + + if(IS_CROSS) + # VCPKG_CROSSCOMPILING is not used since it regresses a lot of ports in x64-windows-x triplets + # For consistency this should proably be changed in the future? + vcpkg_list(APPEND arg_OPTIONS --native "${SCRIPTS}/buildsystems/meson/none.txt") + vcpkg_list(APPEND arg_OPTIONS --cross "${meson_input_file_${buildtype}}") + else() + vcpkg_list(APPEND arg_OPTIONS --native "${meson_input_file_${buildtype}}") + endif() + + # User provided cross/native files + if(VCPKG_MESON_NATIVE_FILE) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE}") + endif() + if(VCPKG_MESON_NATIVE_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE_${buildtype}}") + endif() + if(VCPKG_MESON_CROSS_FILE) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE}") + endif() + if(VCPKG_MESON_CROSS_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE_${buildtype}}") + endif() + + vcpkg_list(APPEND arg_OPTIONS --libdir lib) # else meson install into an architecture describing folder + vcpkg_list(APPEND arg_OPTIONS --pkgconfig.relocatable) + + if(arg_CONFIG STREQUAL "RELEASE") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=false --prefix "${CURRENT_PACKAGES_DIR}") + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug']") + endif() + elseif(arg_CONFIG STREQUAL "DEBUG") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=true --prefix "${CURRENT_PACKAGES_DIR}/debug" --includedir ../include) + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}']") + endif() + else() + message(FATAL_ERROR "Unknown configuration. Only DEBUG and RELEASE are valid values.") + endif() + + # Allow overrides / additional configuration variables from triplets + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS}) + endif() + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}}) + endif() + + if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") + set(MESON_DEFAULT_LIBRARY shared) + else() + set(MESON_DEFAULT_LIBRARY static) + endif() + set(MESON_CMAKE_BUILD_TYPE "${cmake_build_type_${buildtype}}") + z_vcpkg_meson_setup_variables(${buildtype}) + configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/meson.template.in" "${meson_input_file_${buildtype}}" @ONLY) + set("${arg_OUTPUT}" ${arg_OPTIONS} PARENT_SCOPE) +endfunction() + +function(vcpkg_configure_meson) + # parse parameters such that semicolons in options arguments to COMMAND don't get erased + cmake_parse_arguments(PARSE_ARGV 0 arg + "NO_PKG_CONFIG" + "SOURCE_PATH" + "OPTIONS;OPTIONS_DEBUG;OPTIONS_RELEASE;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_NATIVE_BINARIES;ADDITIONAL_CROSS_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(DEFINED arg_ADDITIONAL_NATIVE_BINARIES OR DEFINED arg_ADDITIONAL_CROSS_BINARIES) + message(WARNING "Options ADDITIONAL_(NATIVE|CROSS)_BINARIES have been deprecated. Only use ADDITIONAL_BINARIES!") + endif() + vcpkg_list(APPEND arg_ADDITIONAL_BINARIES ${arg_ADDITIONAL_NATIVE_BINARIES} ${arg_ADDITIONAL_CROSS_BINARIES}) + vcpkg_list(REMOVE_DUPLICATES arg_ADDITIONAL_BINARIES) + + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + + vcpkg_find_acquire_program(MESON) + + get_filename_component(CMAKE_PATH "${CMAKE_COMMAND}" DIRECTORY) + vcpkg_add_to_path("${CMAKE_PATH}") # Make CMake invokeable for Meson + + vcpkg_find_acquire_program(NINJA) + + if(NOT arg_NO_PKG_CONFIG) + vcpkg_find_acquire_program(PKGCONFIG) + set(ENV{PKG_CONFIG} "${PKGCONFIG}") + endif() + + vcpkg_find_acquire_program(PYTHON3) + get_filename_component(PYTHON3_DIR "${PYTHON3}" DIRECTORY) + vcpkg_add_to_path(PREPEND "${PYTHON3_DIR}") + + set(buildtypes "") + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(buildname "DEBUG") + set(cmake_build_type_${buildname} "Debug") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "debug/") + set(suffix_${buildname} "dbg") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + set(buildname "RELEASE") + set(cmake_build_type_${buildname} "Release") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "") + set(suffix_${buildname} "rel") + endif() + + # configure build + foreach(buildtype IN LISTS buildtypes) + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}}") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}") + + vcpkg_generate_meson_cmd_args( + OUTPUT cmd_args + CONFIG ${buildtype} + LANGUAGES ${arg_LANGUAGES} + OPTIONS ${arg_OPTIONS} ${arg_OPTIONS_${buildtype}} + ADDITIONAL_BINARIES ${arg_ADDITIONAL_BINARIES} + ADDITIONAL_PROPERTIES ${arg_ADDITIONAL_PROPERTIES} + ) + + vcpkg_execute_required_process( + COMMAND ${MESON} setup ${cmd_args} ${arg_SOURCE_PATH} + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}" + LOGNAME config-${TARGET_TRIPLET}-${suffix_${buildtype}} + SAVE_LOG_FILES + meson-logs/meson-log.txt + meson-info/intro-dependencies.json + meson-logs/install-log.txt + ) + + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}} done") + endforeach() +endfunction() diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake new file mode 100644 index 000000000..0351f271a --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake @@ -0,0 +1,71 @@ +function(vcpkg_install_meson) + cmake_parse_arguments(PARSE_ARGV 0 arg "ADD_BIN_TO_PATH" "" "") + + vcpkg_find_acquire_program(NINJA) + unset(ENV{DESTDIR}) # installation directory was already specified with '--prefix' option + + if(VCPKG_TARGET_IS_OSX) + vcpkg_backup_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + set(ENV{SDKROOT} "${VCPKG_DETECTED_CMAKE_OSX_SYSROOT}") + set(ENV{MACOSX_DEPLOYMENT_TARGET} "${VCPKG_DETECTED_CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + + foreach(buildtype IN ITEMS "debug" "release") + if(DEFINED VCPKG_BUILD_TYPE AND NOT VCPKG_BUILD_TYPE STREQUAL buildtype) + continue() + endif() + + if(buildtype STREQUAL "debug") + set(short_buildtype "dbg") + else() + set(short_buildtype "rel") + endif() + + message(STATUS "Package ${TARGET_TRIPLET}-${short_buildtype}") + if(arg_ADD_BIN_TO_PATH) + vcpkg_backup_env_variables(VARS PATH) + if(buildtype STREQUAL "debug") + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/debug/bin") + else() + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/bin") + endif() + endif() + vcpkg_execute_required_process( + COMMAND "${NINJA}" install -v + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${short_buildtype}" + LOGNAME package-${TARGET_TRIPLET}-${short_buildtype} + ) + if(arg_ADD_BIN_TO_PATH) + vcpkg_restore_env_variables(VARS PATH) + endif() + endforeach() + + vcpkg_list(SET renamed_libs) + if(VCPKG_TARGET_IS_WINDOWS AND VCPKG_LIBRARY_LINKAGE STREQUAL static AND NOT VCPKG_TARGET_IS_MINGW) + # Meson names all static libraries lib.a which basically breaks the world + file(GLOB_RECURSE gen_libraries "${CURRENT_PACKAGES_DIR}*/**/lib*.a") + foreach(gen_library IN LISTS gen_libraries) + get_filename_component(libdir "${gen_library}" DIRECTORY) + get_filename_component(libname "${gen_library}" NAME) + string(REGEX REPLACE ".a$" ".lib" fixed_librawname "${libname}") + string(REGEX REPLACE "^lib" "" fixed_librawname "${fixed_librawname}") + file(RENAME "${gen_library}" "${libdir}/${fixed_librawname}") + # For cmake fixes. + string(REGEX REPLACE ".a$" "" origin_librawname "${libname}") + string(REGEX REPLACE ".lib$" "" fixed_librawname "${fixed_librawname}") + vcpkg_list(APPEND renamed_libs ${fixed_librawname}) + set(${librawname}_old ${origin_librawname}) + set(${librawname}_new ${fixed_librawname}) + endforeach() + file(GLOB_RECURSE cmake_files "${CURRENT_PACKAGES_DIR}*/*.cmake") + foreach(cmake_file IN LISTS cmake_files) + foreach(current_lib IN LISTS renamed_libs) + vcpkg_replace_string("${cmake_file}" "${${current_lib}_old}" "${${current_lib}_new}" IGNORE_UNCHANGED) + endforeach() + endforeach() + endif() + + if(VCPKG_TARGET_IS_OSX) + vcpkg_restore_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + endif() +endfunction() diff --git a/cmake/vcpkg-triplets/universal-osx.cmake b/cmake/vcpkg-triplets/universal-osx.cmake new file mode 100644 index 000000000..1c91a5650 --- /dev/null +++ b/cmake/vcpkg-triplets/universal-osx.cmake @@ -0,0 +1,8 @@ +# See https://github.com/microsoft/vcpkg/discussions/19454 +# NOTE: Try to keep in sync with default arm64-osx definition +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES "arm64;x86_64") diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json deleted file mode 100644 index 05a4c006d..000000000 --- a/cmake/windowsMSVCPreset.json +++ /dev/null @@ -1,424 +0,0 @@ -{ - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "include": [ - "commonPresets.json" - ], - "configurePresets": [ - { - "name": "windows_msvc_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "generator": "Ninja" - }, - { - "name": "windows_msvc_arm64_cross_base", - "hidden": true, - "inherits": [ - "windows_msvc_base" - ] - }, - { - "name": "windows_msvc_debug", - "inherits": [ - "base_debug", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Debug)" - }, - { - "name": "windows_msvc_release", - "inherits": [ - "base_release", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Release)" - }, - { - "name": "windows_msvc_arm64_cross_debug", - "inherits": [ - "base_debug", - "windows_msvc_arm64_cross_base" - ], - "displayName": "Windows MSVC (ARM64 cross, Debug)" - }, - { - "name": "windows_msvc_arm64_cross_release", - "inherits": [ - "base_release", - "windows_msvc_arm64_cross_base" - ], - "displayName": "Windows MSVC (ARM64 cross, Release)" - }, - { - "name": "windows_msvc_ci", - "inherits": [ - "base_ci", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (CI)" - }, - { - "name": "windows_msvc_arm64_cross_ci", - "inherits": [ - "base_ci", - "windows_msvc_arm64_cross_base" - ], - "displayName": "Windows MSVC (ARM64 cross, CI)" - }, - { - "name": "windows_clang_base", - "hidden": true, - "inherits": [ - "windows_msvc_base" - ], - "cacheVariables": { - "CMAKE_C_COMPILER": "clang-cl", - "CMAKE_CXX_COMPILER": "clang-cl" - } - }, - { - "name": "windows_clang_debug", - "inherits": [ - "base_debug", - "windows_clang_base" - ], - "displayName": "Windows Clang (Debug)" - }, - { - "name": "windows_clang_release", - "inherits": [ - "base_release", - "windows_clang_base" - ], - "displayName": "Windows Clang (Release)" - }, - { - "name": "windows_clang_ci", - "inherits": [ - "base_ci", - "windows_clang_base" - ], - "displayName": "Windows Clang (CI)" - } - ], - "buildPresets": [ - { - "name": "windows_msvc_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "windows_msvc_debug", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Debug)", - "configurePreset": "windows_msvc_debug", - "configuration": "Debug" - }, - { - "name": "windows_msvc_release", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Release)", - "configurePreset": "windows_msvc_release", - "configuration": "Release" - }, - { - "name": "windows_msvc_arm64_cross_debug", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (ARM64 cross, Debug)", - "configurePreset": "windows_msvc_arm64_cross_debug", - "configuration": "Debug" - }, - { - "name": "windows_msvc_arm64_cross_release", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (ARM64 cross, Release)", - "configurePreset": "windows_msvc_arm64_cross_release", - "configuration": "Release" - }, - { - "name": "windows_msvc_ci", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (CI)", - "configurePreset": "windows_msvc_ci", - "configuration": "Release" - }, - { - "name": "windows_msvc_arm64_cross_ci", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows MSVC (ARM64 cross, CI)", - "configurePreset": "windows_msvc_arm64_cross_ci", - "configuration": "Release" - }, - { - "name": "windows_clang_debug", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows Clang (Debug)", - "configurePreset": "windows_clang_debug", - "configuration": "Debug" - }, - { - "name": "windows_clang_release", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows Clang (Release)", - "configurePreset": "windows_clang_release", - "configuration": "Release" - }, - { - "name": "windows_clang_ci", - "inherits": [ - "windows_msvc_base" - ], - "displayName": "Windows Clang (CI)", - "configurePreset": "windows_clang_ci", - "configuration": "Release" - } - ], - "testPresets": [ - { - "name": "windows_msvc_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "windows_msvc_debug", - "inherits": [ - "base_debug", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Debug)", - "configurePreset": "windows_msvc_debug", - "configuration": "Debug" - }, - { - "name": "windows_msvc_release", - "inherits": [ - "base_release", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (Release)", - "configurePreset": "windows_msvc_release", - "configuration": "Release" - }, - { - "name": "windows_msvc_ci", - "inherits": [ - "base_release", - "windows_msvc_base" - ], - "displayName": "Windows MSVC (CI)", - "configurePreset": "windows_msvc_ci", - "configuration": "Release" - }, - { - "name": "windows_clang_debug", - "inherits": [ - "base_debug", - "windows_msvc_base" - ], - "displayName": "Windows Clang (Debug)", - "configurePreset": "windows_clang_debug", - "configuration": "Debug" - }, - { - "name": "windows_clang_release", - "inherits": [ - "base_release", - "windows_msvc_base" - ], - "displayName": "Windows Clang (Release)", - "configurePreset": "windows_clang_release", - "configuration": "Release" - }, - { - "name": "windows_clang_ci", - "inherits": [ - "base_release", - "windows_msvc_base" - ], - "displayName": "Windows Clang (CI)", - "configurePreset": "windows_clang_ci", - "configuration": "Release" - } - ], - "workflowPresets": [ - { - "name": "windows_msvc_debug", - "displayName": "Windows MSVC (Debug)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_debug" - }, - { - "type": "build", - "name": "windows_msvc_debug" - }, - { - "type": "test", - "name": "windows_msvc_debug" - } - ] - }, - { - "name": "windows_msvc", - "displayName": "Windows MSVC (Release)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_release" - }, - { - "type": "build", - "name": "windows_msvc_release" - }, - { - "type": "test", - "name": "windows_msvc_release" - } - ] - }, - { - "name": "windows_msvc_arm64_cross_debug", - "displayName": "Windows MSVC (ARM64 cross, Debug)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_arm64_cross_debug" - }, - { - "type": "build", - "name": "windows_msvc_arm64_cross_debug" - } - ] - }, - { - "name": "windows_msvc_arm64_cross", - "displayName": "Windows MSVC (ARM64 cross, Release)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_arm64_cross_release" - }, - { - "type": "build", - "name": "windows_msvc_arm64_cross_release" - } - ] - }, - { - "name": "windows_msvc_ci", - "displayName": "Windows MSVC (CI)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_ci" - }, - { - "type": "build", - "name": "windows_msvc_ci" - }, - { - "type": "test", - "name": "windows_msvc_ci" - } - ] - }, - { - "name": "windows_msvc_arm64_cross_ci", - "displayName": "Windows MSVC (ARM64 cross, CI)", - "steps": [ - { - "type": "configure", - "name": "windows_msvc_arm64_cross_ci" - }, - { - "type": "build", - "name": "windows_msvc_arm64_cross_ci" - } - ] - }, - { - "name": "windows_clang_debug", - "displayName": "Windows Clang (Debug)", - "steps": [ - { - "type": "configure", - "name": "windows_clang_debug" - }, - { - "type": "build", - "name": "windows_clang_debug" - }, - { - "type": "test", - "name": "windows_clang_debug" - } - ] - }, - { - "name": "windows_clang_release", - "displayName": "Windows Clang (Release)", - "steps": [ - { - "type": "configure", - "name": "windows_clang_release" - }, - { - "type": "build", - "name": "windows_clang_release" - }, - { - "type": "test", - "name": "windows_clang_release" - } - ] - }, - { - "name": "windows_clang_ci", - "displayName": "Windows Clang (CI)", - "steps": [ - { - "type": "configure", - "name": "windows_clang_ci" - }, - { - "type": "build", - "name": "windows_clang_ci" - }, - { - "type": "test", - "name": "windows_clang_ci" - } - ] - } - ] -} diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json deleted file mode 100644 index 7c4adbcf2..000000000 --- a/cmake/windowsMinGWPreset.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", - "version": 8, - "include": [ - "commonPresets.json" - ], - "configurePresets": [ - { - "name": "windows_mingw_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "generator": "Ninja" - }, - { - "name": "windows_mingw_debug", - "inherits": [ - "base_debug", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Debug)" - }, - { - "name": "windows_mingw_release", - "inherits": [ - "base_release", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Release)" - }, - { - "name": "windows_mingw_ci", - "inherits": [ - "base_ci", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (CI)" - } - ], - "buildPresets": [ - { - "name": "windows_mingw_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - } - }, - { - "name": "windows_mingw_debug", - "inherits": [ - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Debug)", - "configurePreset": "windows_mingw_debug" - }, - { - "name": "windows_mingw_release", - "inherits": [ - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Release)", - "configurePreset": "windows_mingw_release" - }, - { - "name": "windows_mingw_ci", - "inherits": [ - "windows_mingw_base" - ], - "displayName": "Windows MinGW (CI)", - "configurePreset": "windows_mingw_ci" - } - ], - "testPresets": [ - { - "name": "windows_mingw_base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "filter": { - "exclude": { - "name": "^example64|example$" - } - } - }, - { - "name": "windows_mingw_debug", - "inherits": [ - "base_debug", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Debug)", - "configurePreset": "windows_mingw_debug" - }, - { - "name": "windows_mingw_release", - "inherits": [ - "base_release", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (Release)", - "configurePreset": "windows_mingw_release" - }, - { - "name": "windows_mingw_ci", - "inherits": [ - "base_release", - "windows_mingw_base" - ], - "displayName": "Windows MinGW (CI)", - "configurePreset": "windows_mingw_ci" - } - ], - "workflowPresets": [ - { - "name": "windows_mingw_debug", - "displayName": "Windows MinGW (Debug)", - "steps": [ - { - "type": "configure", - "name": "windows_mingw_debug" - }, - { - "type": "build", - "name": "windows_mingw_debug" - }, - { - "type": "test", - "name": "windows_mingw_debug" - } - ] - }, - { - "name": "windows_mingw", - "displayName": "Windows MinGW (Release)", - "steps": [ - { - "type": "configure", - "name": "windows_mingw_release" - }, - { - "type": "build", - "name": "windows_mingw_release" - }, - { - "type": "test", - "name": "windows_mingw_release" - } - ] - }, - { - "name": "windows_mingw_ci", - "displayName": "Windows MinGW (CI)", - "steps": [ - { - "type": "configure", - "name": "windows_mingw_ci" - }, - { - "type": "build", - "name": "windows_mingw_ci" - }, - { - "type": "test", - "name": "windows_mingw_ci" - } - ] - } - ] -} diff --git a/launcher/Application.cpp b/launcher/Application.cpp index aae212891..fd5f6c22a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -46,12 +46,12 @@ #include "DataMigrationTask.h" #include "java/JavaInstallList.h" #include "net/PasteUpload.h" -#include "pathmatcher/MultiMatcher.h" -#include "pathmatcher/SimplePrefixMatcher.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" +#include "ui/ToolTipFilter.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" @@ -59,8 +59,7 @@ #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" -#include "ui/pages/global/CustomCommandsPage.h" -#include "ui/pages/global/EnvironmentVariablesPage.h" +#include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" @@ -98,6 +97,7 @@ #include #include #include +#include #include #include #include @@ -109,8 +109,6 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" -#include "java/JavaInstallList.h" - #include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" @@ -155,11 +153,16 @@ #endif #if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif #include #include -#include "WindowsConsole.h" +#include "console/WindowsConsole.h" #endif +#include "console/Console.h" + #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) @@ -167,6 +170,63 @@ static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; +static bool isANSIColorConsole; + +static QString defaultLogFormat = QStringLiteral( + "%{time process}" + " " + "%{if-debug}Debug:%{endif}" + "%{if-info}Info:%{endif}" + "%{if-warning}Warning:%{endif}" + "%{if-critical}Critical:%{endif}" + "%{if-fatal}Fatal:%{endif}" + " " + "%{if-category}[%{category}] %{endif}" + "%{message}" + " " + "(%{function}:%{line})"); + +#define ansi_reset "\x1b[0m" +#define ansi_bold "\x1b[1m" +#define ansi_reset_bold "\x1b[22m" +#define ansi_faint "\x1b[2m" +#define ansi_italic "\x1b[3m" +#define ansi_red_fg "\x1b[31m" +#define ansi_green_fg "\x1b[32m" +#define ansi_yellow_fg "\x1b[33m" +#define ansi_blue_fg "\x1b[34m" +#define ansi_purple_fg "\x1b[35m" +#define ansi_inverse "\x1b[7m" + +// clang-format off +static QString ansiLogFormat = QStringLiteral( + ansi_faint "%{time process}" ansi_reset + " " + "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" + "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" + "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" + "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" + "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" + " " + "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" + "%{message}" + " " + ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset +); +// clang-format on + +#undef ansi_inverse +#undef ansi_purple_fg +#undef ansi_blue_fg +#undef ansi_yellow_fg +#undef ansi_green_fg +#undef ansi_red_fg +#undef ansi_italic +#undef ansi_faint +#undef ansi_bold +#undef ansi_reset_bold +#undef ansi_reset + namespace { /** This is used so that we can output to the log file in addition to the CLI. */ @@ -175,11 +235,27 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + if (isANSIColorConsole) { + // ensure default is set for log file + qSetMessagePattern(defaultLogFormat); + } + QString out = qFormatLogMessage(type, context, msg); - out += QChar::LineFeed; + if (APPLICATION->logModel) { + APPLICATION->logModel->append(MessageLevel::fromQtMsgType(type), out); + } + out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); + + if (isANSIColorConsole) { + // format ansi for console; + qSetMessagePattern(ansiLogFormat); + out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + } + QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } @@ -220,14 +296,24 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // attach the parent console if stdout not already captured if (AttachWindowsConsole()) { consoleAttached = true; + if (auto err = EnableAnsiSupport(); !err) { + isANSIColorConsole = true; + } else { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +#else + if (console::isConsole()) { + isANSIColorConsole = true; } #endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); - setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); + setDesktopFileName(BuildConfig.LAUNCHER_APPID); m_startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window @@ -244,6 +330,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, + { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); @@ -259,6 +346,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_serverToJoin = parser.value("server"); m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); + if (parser.isSet("offline")) { + m_offline = true; + m_offlineName = parser.value("offline"); + } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); @@ -273,8 +364,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // error if --launch is missing with --server or --profile - if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { - std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; + if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) && + m_instanceIdToLaunch.isEmpty()) { + std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; } @@ -371,19 +463,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { + bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; - m_peerInstance->sendMessage(activate.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); - m_peerInstance->sendMessage(import.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { @@ -399,10 +492,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } - m_peerInstance->sendMessage(launch.serialize(), timeout); + if (m_offline) { + launch.args["offline_enabled"] = "true"; + launch.args["offline_name"] = m_offlineName; + } + sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); + } + if (sentMessage) { + m_status = Application::Succeeded; + return; + } else { + std::cerr << "Unable to redirect command to already running instance\n"; + // C function not Qt function - event loop not started yet + ::exit(1); } - m_status = Application::Succeeded; - return; } } @@ -433,27 +536,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) return; } qInstallMessageHandler(appDebugOutput); + qSetMessagePattern(defaultLogFormat); - qSetMessagePattern( - "%{time process}" - " " - "%{if-debug}D%{endif}" - "%{if-info}I%{endif}" - "%{if-warning}W%{endif}" - "%{if-critical}C%{endif}" - "%{if-fatal}F%{endif}" - " " - "|" - " " - "%{if-category}[%{category}]: %{endif}" - "%{message}"); + logModel.reset(new LogModel(this)); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() @@ -461,7 +553,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { - qDebug() << "Found" << logRulesPath << "..."; + qInfo() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } @@ -472,28 +564,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules - qDebug() << "Loading logging rules from:" << logRulesPath; + qInfo() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; - qDebug() << "Setting log rules:"; + qInfo() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); - qDebug() << " " << rule; + qInfo() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } - qDebug() << "<> Log initialized."; + qInfo() << "<> Log initialized."; } { @@ -510,33 +602,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); - qDebug() << "Version : " << BuildConfig.printableVersionString(); - qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; - qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; - qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; - qDebug() << "Compiled for : " << BuildConfig.systemID(); - qDebug() << "Compiled by : " << BuildConfig.compilerID(); - qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; - qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); + qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qInfo() << "Version : " << BuildConfig.printableVersionString(); + qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM; + qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT; + qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qInfo() << "Compiled for : " << BuildConfig.systemID(); + qInfo() << "Compiled by : " << BuildConfig.compilerID(); + qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { - qDebug() << "Work dir before adjustment : " << origcwdPath; - qDebug() << "Work dir after adjustment : " << QDir::currentPath(); - qDebug() << "Adjusted by : " << adjustedBy; + qInfo() << "Work dir before adjustment : " << origcwdPath; + qInfo() << "Work dir after adjustment : " << QDir::currentPath(); + qInfo() << "Adjusted by : " << adjustedBy; } else { - qDebug() << "Work dir : " << QDir::currentPath(); + qInfo() << "Work dir : " << QDir::currentPath(); } - qDebug() << "Binary path : " << binPath; - qDebug() << "Application root path : " << m_rootPath; + qInfo() << "Binary path : " << binPath; + qInfo() << "Application root path : " << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { - qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; + qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { - qDebug() << "Address of server to join :" << m_serverToJoin; + qInfo() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { - qDebug() << "Name of the world to join :" << m_worldToJoin; + qInfo() << "Name of the world to join :" << m_worldToJoin; } - qDebug() << "<> Paths set."; + qInfo() << "<> Paths set."; } if (m_liveCheck) { @@ -601,15 +693,30 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); + logModel->setMaxLines(getConsoleMaxLines(settings())); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); + logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); + // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); + m_settings->registerSetting("MoveModsFromDownloadsDir", false); m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("JavaDir", "java"); +#ifdef Q_OS_MACOS + // Folder security-scoped bookmarks + m_settings->registerSetting("InstanceDirBookmark", ""); + m_settings->registerSetting("CentralModsDirBookmark", ""); + m_settings->registerSetting("IconsDirBookmark", ""); + m_settings->registerSetting("DownloadsDirBookmark", ""); + m_settings->registerSetting("SkinsDirBookmark", ""); + m_settings->registerSetting("JavaDirBookmark", ""); +#endif + // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -695,12 +802,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); m_settings->registerSetting("CatOpacity", 100); + m_settings->registerSetting("CatFit", "fit"); m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("ToolbarsLocked", false); + // Instance m_settings->registerSetting("InstSortMode", "Name"); + m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry @@ -722,6 +832,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); + m_settings->registerSetting("DataPackDownloadGeometry", ""); + + // data pack window + // in future, more pages may be added - so this name is chosen to avoid needing migration + m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { @@ -755,12 +870,21 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // get rid of invalid meta urls if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) m_settings->reset("MetaURLOverride"); + + // Resource URL + m_settings->registerSetting("ResourceURL", BuildConfig.DEFAULT_RESOURCE_BASE); + + 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"); } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); - m_settings->registerSetting("Env", QVariant(QMap())); + m_settings->registerSetting("Env", "{}"); // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); @@ -787,22 +911,21 @@ 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(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); - qDebug() << "<> Settings loaded."; + qInfo() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY @@ -818,7 +941,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); - qDebug() << "<> Network done."; + qInfo() << "<> Network done."; } // load translations @@ -826,8 +949,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_translations.reset(new TranslationsModel("translations")); auto bcp47Name = m_settings->get("Language").toString(); m_translations->selectLanguage(bcp47Name); - qDebug() << "Your language is" << bcp47Name; - qDebug() << "<> Translations loaded."; + qInfo() << "Your language is" << bcp47Name; + qInfo() << "<> Translations loaded."; } // Instance icons @@ -837,38 +960,53 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) ":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" }; m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, - [&](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); - qDebug() << "<> Instance icons initialized."; + [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); + qInfo() << "<> Instance icons initialized."; } // Themes m_themeManager = std::make_unique(); +#ifdef Q_OS_MACOS + // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present + // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory, + // and a sandboxed version that can't access the directory without a bookmark + // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore + { + m_settings->get("InstanceDir"); + m_settings->get("CentralModsDir"); + m_settings->get("IconsDir"); + m_settings->get("DownloadsDir"); + m_settings->get("SkinsDir"); + m_settings->get("JavaDir"); + } +#endif + // initialize and load all instances { auto InstDirSetting = m_settings->getSetting("InstanceDir"); // instance path: check for problems with '!' in instance path and warn the user in the log // and remember that we have to show him a dialog when the gui starts (if it does so) - QString instDir = InstDirSetting->get().toString(); - qDebug() << "Instance path : " << instDir; + QString instDir = m_settings->get("InstanceDir").toString(); + qInfo() << "Instance path : " << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } - m_instances.reset(new InstanceList(m_settings, instDir, this)); + m_instances.reset(new InstanceList(m_settings.get(), instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); - qDebug() << "Loading Instances..."; + qInfo() << "Loading Instances..."; m_instances->loadList(); - qDebug() << "<> Instances loaded."; + qInfo() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); - qDebug() << "Loading accounts..."; + qInfo() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); - qDebug() << "<> Accounts loaded."; + qInfo() << "<> Accounts loaded."; } // init the http meta cache @@ -889,7 +1027,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); - qDebug() << "<> Cache initialized."; + qInfo() << "<> Cache initialized."; } // now we have network, download translation updates @@ -900,12 +1038,12 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) 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 @@ -1062,6 +1200,10 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } } + if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { + installEventFilter(new ToolTipFilter); + } + if (createSetupWizard()) { return; } @@ -1072,11 +1214,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { - bool javaRequired = [&]() { - if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) { + bool javaRequired = [this]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { return false; } - bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); + bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } @@ -1090,8 +1232,8 @@ bool Application::createSetupWizard() QString actualPath = FS::ResolveExecutable(currentJavaPath); return actualPath.isNull(); }(); - bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() && - !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool(); + bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() && + !settings()->get("AutomaticJavaSwitch").toBool() && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); @@ -1222,7 +1364,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, targetToJoin, accountToUse); + launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); return; } } @@ -1324,8 +1466,10 @@ void Application::messageReceived(const QByteArray& message) QString server = received.args["server"]; QString world = received.args["world"]; QString profile = received.args["profile"]; + bool offline = received.args["offline_enabled"] == "true"; + QString offlineName = received.args["offline_name"]; - InstancePtr instance; + BaseInstance* instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { @@ -1353,31 +1497,28 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, true, false, serverObject, accountObject); + launch(instance, !offline, false, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } } -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::getThemedIcon(const QString& name) +QIcon Application::logo() { - if (name == "logo") { - return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); - } - return QIcon::fromTheme(name); + return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); } bool Application::openJsonEditor(const QString& filename) @@ -1391,7 +1532,12 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse) +bool Application::launch(BaseInstance* instance, + bool online, + bool demo, + MinecraftTarget::Ptr targetToJoin, + MinecraftAccountPtr accountToUse, + const QString& offlineName) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; @@ -1412,14 +1558,13 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); + controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { 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; @@ -1433,7 +1578,7 @@ bool Application::launch(InstancePtr instance, bool online, bool demo, Minecraft 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."; @@ -1442,7 +1587,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(); @@ -1491,18 +1636,19 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -void Application::controllerSucceeded() +void Application::controllerFinished() { - auto controller = qobject_cast(QObject::sender()); + auto controller = qobject_cast(sender()); if (!controller) return; 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); } @@ -1512,29 +1658,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(QObject::sender()); - if (!controller) - return; - auto id = controller->id(); - QMutexLocker locker(&m_instanceExtrasMutex); - auto& extras = m_instanceExtras[id]; - - // on failure, do... nothing - extras.controller.reset(); - subRunningInstance(); - - // quit when there are no more windows. - if (shouldExitNow()) { - m_status = Status::Failed; - exit(1); + m_status = wasSuccessful ? Succeeded : Failed; + exit(wasSuccessful ? 0 : 1); } } @@ -1547,9 +1672,9 @@ void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); + connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); dlg.exec(); } - emit globalSettingsClosed(); } MainWindow* Application::showMainWindow(bool minimized) @@ -1560,8 +1685,8 @@ MainWindow* Application::showMainWindow(bool minimized) m_mainWindow->activateWindow(); } else { m_mainWindow = new MainWindow(); - m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); - m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); + m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); + m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); if (minimized) { m_mainWindow->showMinimized(); @@ -1577,7 +1702,21 @@ MainWindow* Application::showMainWindow(bool minimized) return m_mainWindow; } -InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) +ViewLogWindow* Application::showLogWindow() +{ + if (m_viewLogWindow) { + m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); + m_viewLogWindow->raise(); + m_viewLogWindow->activateWindow(); + } else { + m_viewLogWindow = new ViewLogWindow(); + connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + return m_viewLogWindow; +} + +InstanceWindow* Application::showInstanceWindow(BaseInstance* instance, QString page) { if (!instance) return nullptr; @@ -1618,7 +1757,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa void Application::on_windowClose() { m_openWindows--; - auto instWindow = qobject_cast(QObject::sender()); + auto instWindow = qobject_cast(sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; @@ -1627,10 +1766,14 @@ void Application::on_windowClose() extras.controller->setParentWidget(m_mainWindow); } } - auto mainWindow = qobject_cast(QObject::sender()); + auto mainWindow = qobject_cast(sender()); if (mainWindow) { m_mainWindow = nullptr; } + auto logWindow = qobject_cast(sender()); + if (logWindow) { + m_viewLogWindow = nullptr; + } // quit when there are no more windows. if (shouldExitNow()) { exit(0); @@ -1685,22 +1828,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() @@ -1785,17 +1928,6 @@ QString Application::getUserAgent() return BuildConfig.USER_AGENT; } -QString Application::getUserAgentUncached() -{ - QString uaOverride = m_settings->get("UserAgentOverride").toString(); - if (!uaOverride.isEmpty()) { - uaOverride += " (Uncached)"; - return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); - } - - return BuildConfig.USER_AGENT_UNCACHED; -} - bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, @@ -1841,7 +1973,9 @@ bool Application::handleDataMigration(const QString& currentData, auto setDoNotMigrate = [&nomigratePath] { QFile file(nomigratePath); - file.open(QIODevice::WriteOnly); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; + } }; // create no-migrate file if user doesn't want to migrate @@ -1853,22 +1987,23 @@ bool Application::handleDataMigration(const QString& currentData, if (!currentExists) { // Migrate! - auto matcher = std::make_shared(); - matcher->add(std::make_shared(configFile)); - matcher->add(std::make_shared( - BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before - matcher->add(std::make_shared("logs/")); - matcher->add(std::make_shared("accounts.json")); - matcher->add(std::make_shared("accounts/")); - matcher->add(std::make_shared("assets/")); - matcher->add(std::make_shared("icons/")); - matcher->add(std::make_shared("instances/")); - matcher->add(std::make_shared("libraries/")); - matcher->add(std::make_shared("mods/")); - matcher->add(std::make_shared("themes/")); + using namespace Filters; + + QList filters; + filters.append(equals(configFile)); + filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + filters.append(startsWith("logs/")); + filters.append(equals("accounts.json")); + filters.append(startsWith("accounts/")); + filters.append(startsWith("assets/")); + filters.append(startsWith("icons/")); + filters.append(startsWith("instances/")); + filters.append(startsWith("libraries/")); + filters.append(startsWith("mods/")); + filters.append(startsWith("themes/")); ProgressDialog diag; - DataMigrationTask task(oldData, currentData, matcher); + DataMigrationTask task(oldData, currentData, any(std::move(filters))); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); @@ -1934,4 +2069,4 @@ bool Application::checkQSavePath(QString path) } } return false; -} \ No newline at end of file +} diff --git a/launcher/Application.h b/launcher/Application.h index ba26dc607..041e5b892 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -48,12 +48,14 @@ #include +#include "launch/LogModel.h" #include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; +class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; @@ -110,29 +112,29 @@ 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()); } - QIcon getThemedIcon(const QString& name); + QIcon logo(); 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; } @@ -140,11 +142,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(); @@ -160,7 +162,6 @@ class Application : public QApplication { QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); - QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } @@ -181,8 +182,9 @@ 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(); void updateIsRunning(bool running); bool updatesAreAllowed(); @@ -197,7 +199,7 @@ class Application : public QApplication { signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); - void globalSettingsClosed(); + void globalSettingsApplied(); int currentCatChanged(int index); void oauthReplyRecieved(QVariantMap); @@ -207,19 +209,19 @@ class Application : public QApplication { #endif public slots: - bool launch(InstancePtr instance, + bool launch(BaseInstance* instance, bool online = true, bool demo = false, MinecraftTarget::Ptr targetToJoin = nullptr, - MinecraftAccountPtr accountToUse = nullptr); - bool kill(InstancePtr instance); + MinecraftAccountPtr accountToUse = nullptr, + const QString& offlineName = QString()); + 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: @@ -235,23 +237,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; @@ -276,7 +282,7 @@ class Application : public QApplication { // 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; @@ -289,6 +295,9 @@ class Application : public QApplication { // main window, if any MainWindow* m_mainWindow = nullptr; + // log window, if any + ViewLogWindow* m_viewLogWindow = nullptr; + // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; @@ -301,17 +310,16 @@ class Application : public QApplication { QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; + bool m_offline = false; + QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + std::unique_ptr logModel; public: void addQSavePath(QString); void removeQSavePath(QString); bool checkQSavePath(QString); - - private: - QHash m_qsaveResources; - mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 69cf95e3c..dc2eb2da3 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -42,8 +42,10 @@ #include #include #include -#include +#include "Application.h" +#include "Json.h" +#include "launch/LaunchTask.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -52,9 +54,26 @@ #include "Commandline.h" #include "FileSystem.h" -BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() +int getConsoleMaxLines(SettingsObject* settings) { - m_settings = settings; + auto lineSetting = settings->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool shouldStopOnConsoleOverflow(SettingsObject* settings) +{ + return settings->get("ConsoleOverflowStop").toBool(); +} + +BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptr settings, const QString& rootDir) : QObject() +{ + m_settings = std::move(settings); m_global_settings = globalSettings; m_rootDir = rootDir; @@ -69,6 +88,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); + m_settings->registerSetting("shortcuts", QString()); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); @@ -107,6 +127,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("Profiler", ""); } +BaseInstance::~BaseInstance() {} + QString BaseInstance::getPreLaunchCommand() { return settings()->get("PreLaunchCommand").toString(); @@ -174,46 +196,35 @@ void BaseInstance::copyManagedPack(BaseInstance& other) m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); -} -int BaseInstance::getConsoleMaxLines() const -{ - auto lineSetting = m_settings->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && + m_settings->get("OverrideJavaLocation").toBool()) { + m_settings->set("OverrideJavaLocation", false); + m_settings->set("JavaPath", ""); } - return maxLines; -} - -bool BaseInstance::shouldStopOnConsoleOverflow() const -{ - return m_settings->get("ConsoleOverflowStop").toBool(); } QStringList BaseInstance::getLinkedInstances() const { - return m_settings->get("linkedInstances").toStringList(); + auto setting = m_settings->get("linkedInstances").toString(); + return Json::toStringList(setting); } void BaseInstance::setLinkedInstances(const QStringList& list) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); - m_settings->set("linkedInstances", list); + m_settings->set("linkedInstances", Json::fromStringList(list)); } void BaseInstance::addLinkedInstanceId(const QString& id) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); linkedInstances.append(id); setLinkedInstances(linkedInstances); } bool BaseInstance::removeLinkedInstanceId(const QString& id) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); int numRemoved = linkedInstances.removeAll(id); setLinkedInstances(linkedInstances); return numRemoved > 0; @@ -221,7 +232,7 @@ bool BaseInstance::removeLinkedInstanceId(const QString& id) bool BaseInstance::isLinkedToInstanceId(const QString& id) const { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); return linkedInstances.contains(id); } @@ -327,11 +338,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 @@ -386,6 +397,63 @@ void BaseInstance::setName(QString val) emit propertiesChanged(this); } +bool BaseInstance::syncInstanceDirName(const QString& newRoot) const +{ + auto oldRoot = instanceRoot(); + return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); +} + +void BaseInstance::registerShortcut(const ShortcutData& data) +{ + auto currentShortcuts = shortcuts(); + currentShortcuts.append(data); + qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; + setShortcuts(currentShortcuts); +} + +void BaseInstance::setShortcuts(const QList& shortcuts) +{ + // FIXME: if no change, do not set. setting involves saving a file. + QJsonArray array; + for (const auto& elem : shortcuts) { + array.append(QJsonObject{ { "name", elem.name }, { "filePath", elem.filePath }, { "target", static_cast(elem.target) } }); + } + + QJsonDocument document; + document.setArray(array); + m_settings->set("shortcuts", QString::fromUtf8(document.toJson(QJsonDocument::Compact))); +} + +QList BaseInstance::shortcuts() const +{ + auto data = m_settings->get("shortcuts").toString().toUtf8(); + QJsonParseError parseError; + auto document = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isArray()) + return {}; + + QList results; + for (const auto& elem : document.array()) { + if (!elem.isObject()) + continue; + auto dict = elem.toObject(); + if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) + continue; + int value = dict["target"].toInt(-1); + if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) + continue; + + QString shortcutName = dict["name"].toString(); + QString filePath = dict["filePath"].toString(); + if (!QDir(filePath).exists()) { + qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" << filePath; + continue; + } + results.append({ shortcutName, filePath, static_cast(value) }); + } + return results; +} + QString BaseInstance::name() const { return m_settings->get("name").toString(); @@ -402,12 +470,17 @@ 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() { // NOOP } + +bool BaseInstance::isLegacy() +{ + return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); +} diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 2be28d1ec..9280d2e1c 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -38,7 +38,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -50,7 +52,6 @@ #include "BaseVersionList.h" #include "MessageLevel.h" #include "minecraft/auth/MinecraftAccount.h" -#include "pathmatcher/IPathMatcher.h" #include "settings/INIFile.h" #include "net/Mode.h" @@ -63,8 +64,19 @@ 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 }; + +/// Shortcut data representation +struct ShortcutData { + QString name; + QString filePath; + ShortcutTarget target = ShortcutTarget::Other; +}; + +/// Console settings +int getConsoleMaxLines(SettingsObject* settings); +bool shouldStopOnConsoleOverflow(SettingsObject* settings); /*! * \brief Base class for instances. @@ -74,11 +86,11 @@ using InstancePtr = std::shared_ptr; * 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 { @@ -88,7 +100,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this shortcuts() const; + void setShortcuts(const QList& shortcuts); + /// Value used for instance window titles QString windowTitle() const; @@ -148,9 +168,6 @@ 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 @@ -195,15 +212,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_this); + void launchTaskChanged(LaunchTask*); void runningStatusChanged(bool running); @@ -296,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; @@ -309,7 +320,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this; virtual ~BaseVersion() {} /*! * A string used to identify this version in config files. * This should be unique within the version list or shenanigans will occur. */ - virtual QString descriptor() = 0; + virtual QString descriptor() const = 0; /*! * The name of this version as it is displayed to the user. * For example: "1.5.1" */ - virtual QString name() = 0; + virtual QString name() const = 0; /*! * This should return a string that describes * the kind of version this is (Stable, Beta, Snapshot, whatever) */ virtual QString typeString() const = 0; - virtual bool operator<(BaseVersion& a) { return name() < a.name(); } - virtual bool operator>(BaseVersion& a) { return name() > a.name(); } + virtual bool operator<(BaseVersion& a) const { return name() < a.name(); } + virtual bool operator>(BaseVersion& a) const { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 982ff7b6b..a15560e46 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -21,11 +21,19 @@ set(CORE_SOURCES BaseVersion.h BaseInstance.h BaseInstance.cpp + InstanceDirUpdate.h + InstanceDirUpdate.cpp NullInstance.h MMCZip.h MMCZip.cpp - Untar.h - Untar.cpp + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h + archive/ExportToZipTask.cpp + archive/ExportToZipTask.h + archive/ExtractZipTask.cpp + archive/ExtractZipTask.h StringUtils.h StringUtils.cpp QVariantUtils.h @@ -54,7 +62,6 @@ set(CORE_SOURCES # String filters Filter.h - Filter.cpp # JSON parsing helpers Json.h @@ -97,7 +104,7 @@ set(CORE_SOURCES MTPixmapCache.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) -set(CORE_SOURCES + set(CORE_SOURCES ${CORE_SOURCES} # MangoHud @@ -106,21 +113,13 @@ set(CORE_SOURCES ) endif() -set(PATHMATCHER_SOURCES - # Path matchers - pathmatcher/FSTreeMatcher.h - pathmatcher/IPathMatcher.h - pathmatcher/MultiMatcher.h - pathmatcher/RegexpMatcher.h - pathmatcher/SimplePrefixMatcher.h -) - set(NET_SOURCES # network stuffs net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h + net/DummySink.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp @@ -173,6 +172,8 @@ set(LAUNCH_SOURCES launch/LogModel.h launch/TaskStepWrapper.cpp launch/TaskStepWrapper.h + logs/LogParser.cpp + logs/LogParser.h ) # Old update system @@ -248,9 +249,6 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h - minecraft/gameoptions/GameOptions.h - minecraft/gameoptions/GameOptions.cpp - minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp minecraft/update/FMLLibrariesTask.cpp @@ -306,6 +304,8 @@ set(MINECRAFT_SOURCES minecraft/ParseUtils.h minecraft/ProfileUtils.cpp minecraft/ProfileUtils.h + minecraft/ShortcutUtils.cpp + minecraft/ShortcutUtils.h minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h @@ -332,6 +332,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp + minecraft/mod/DataPackFolderModel.h + minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h @@ -345,17 +347,15 @@ set(MINECRAFT_SOURCES minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h - minecraft/mod/tasks/BasicFolderLoadTask.h - minecraft/mod/tasks/ModFolderLoadTask.h - minecraft/mod/tasks/ModFolderLoadTask.cpp + minecraft/mod/ShaderPackFolderModel.cpp + minecraft/mod/tasks/ResourceFolderLoadTask.h + minecraft/mod/tasks/ResourceFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.cpp - minecraft/mod/tasks/LocalModUpdateTask.h - minecraft/mod/tasks/LocalModUpdateTask.cpp + minecraft/mod/tasks/LocalResourceUpdateTask.h + minecraft/mod/tasks/LocalResourceUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp - minecraft/mod/tasks/LocalResourcePackParseTask.h - minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -489,8 +489,11 @@ set(META_SOURCES set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp + modplatform/ResourceType.h + modplatform/ResourceType.cpp modplatform/ResourceAPI.h + modplatform/ResourceAPI.cpp modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp @@ -501,8 +504,6 @@ set(API_SOURCES modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp - modplatform/helpers/NetworkResourceAPI.h - modplatform/helpers/NetworkResourceAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h @@ -530,8 +531,6 @@ set(FTB_SOURCES set(FLAME_SOURCES # Flame - modplatform/flame/FlamePackIndex.cpp - modplatform/flame/FlamePackIndex.h modplatform/flame/FlameModIndex.cpp modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h @@ -549,8 +548,6 @@ set(FLAME_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h - modplatform/modrinth/ModrinthPackManifest.cpp - modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -588,8 +585,8 @@ set(ATLAUNCHER_SOURCES ) set(LINKEXE_SOURCES - WindowsConsole.cpp - WindowsConsole.h + console/WindowsConsole.h + console/WindowsConsole.cpp filelink/FileLink.h filelink/FileLink.cpp @@ -626,6 +623,10 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h # Time MMCTime.h @@ -658,6 +659,14 @@ set(PRISMUPDATER_SOURCES ) +if(WIN32) + set(PRISMUPDATER_SOURCES + console/WindowsConsole.h + console/WindowsConsole.cpp + ${PRISMUPDATER_SOURCES} + ) +endif() + ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -745,7 +754,6 @@ endif() set(LOGIC_SOURCES ${CORE_SOURCES} - ${PATHMATCHER_SOURCES} ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} @@ -785,6 +793,9 @@ SET(LAUNCHER_SOURCES SysInfo.h SysInfo.cpp + # console utils + console/Console.h + # GUI - general utilities DesktopServices.h DesktopServices.cpp @@ -811,7 +822,8 @@ SET(LAUNCHER_SOURCES resources/flat/flat.qrc resources/flat_white/flat_white.qrc resources/documents/documents.qrc - ../${Launcher_Branding_LogoQRC} + resources/shaders/shaders.qrc + "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" # Icons icons/MMCIcon.h @@ -819,6 +831,10 @@ SET(LAUNCHER_SOURCES icons/IconList.h icons/IconList.cpp + # log utils + logs/AnonymizeLog.cpp + logs/AnonymizeLog.h + # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp @@ -826,6 +842,10 @@ SET(LAUNCHER_SOURCES ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp + ui/ViewLogWindow.h + ui/ViewLogWindow.cpp + ui/ToolTipFilter.h + ui/ToolTipFilter.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp @@ -843,7 +863,6 @@ SET(LAUNCHER_SOURCES ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h - ui/setupwizard/ThemeWizardPage.cpp ui/setupwizard/ThemeWizardPage.h ui/setupwizard/AutoJavaWizardPage.cpp ui/setupwizard/AutoJavaWizardPage.h @@ -871,6 +890,8 @@ SET(LAUNCHER_SOURCES ui/themes/ThemeManager.h ui/themes/CatPack.cpp ui/themes/CatPack.h + ui/themes/CatPainter.cpp + ui/themes/CatPainter.h # Processes LaunchController.h @@ -891,12 +912,12 @@ SET(LAUNCHER_SOURCES # GUI - instance pages ui/pages/instance/ExternalResourcesPage.cpp ui/pages/instance/ExternalResourcesPage.h - ui/pages/instance/GameOptionsPage.cpp - ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h + ui/pages/instance/DataPackPage.h + ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h @@ -909,7 +930,6 @@ SET(LAUNCHER_SOURCES ui/pages/instance/NotesPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h - ui/pages/instance/InstanceSettingsPage.cpp ui/pages/instance/InstanceSettingsPage.h ui/pages/instance/ScreenshotsPage.cpp ui/pages/instance/ScreenshotsPage.h @@ -919,24 +939,26 @@ SET(LAUNCHER_SOURCES ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h + ui/pages/instance/McClient.cpp + ui/pages/instance/McClient.h + ui/pages/instance/McResolver.cpp + ui/pages/instance/McResolver.h + ui/pages/instance/ServerPingTask.cpp + ui/pages/instance/ServerPingTask.h # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h - ui/pages/global/CustomCommandsPage.cpp - ui/pages/global/CustomCommandsPage.h - ui/pages/global/EnvironmentVariablesPage.cpp - ui/pages/global/EnvironmentVariablesPage.h ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.h ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.h ui/pages/global/LanguagePage.cpp ui/pages/global/LanguagePage.h - ui/pages/global/MinecraftPage.cpp ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h + ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h ui/pages/global/APIPage.cpp @@ -966,6 +988,8 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp + ui/pages/modplatform/DataPackPage.cpp + ui/pages/modplatform/DataPackModel.cpp ui/pages/modplatform/ModpackProviderBasePage.h @@ -1015,8 +1039,6 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/OptionalModDialog.cpp ui/pages/modplatform/OptionalModDialog.h - ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp - ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -1029,6 +1051,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ProfileSetupDialog.h ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.h + ui/dialogs/CreateShortcutDialog.cpp + ui/dialogs/CreateShortcutDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h ui/dialogs/ExportInstanceDialog.cpp @@ -1043,8 +1067,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ImportResourceDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h - ui/dialogs/OfflineLoginDialog.cpp - ui/dialogs/OfflineLoginDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp @@ -1067,14 +1089,23 @@ SET(LAUNCHER_SOURCES ui/dialogs/BlockedModsDialog.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp - ui/dialogs/ModUpdateDialog.cpp - ui/dialogs/ModUpdateDialog.h + ui/dialogs/ResourceUpdateDialog.cpp + ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + ui/dialogs/ChooseOfflineNameDialog.cpp + ui/dialogs/ChooseOfflineNameDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h + ui/dialogs/skins/draw/SkinOpenGLWindow.h + ui/dialogs/skins/draw/SkinOpenGLWindow.cpp + ui/dialogs/skins/draw/Scene.h + ui/dialogs/skins/draw/Scene.cpp + ui/dialogs/skins/draw/BoxGeometry.h + ui/dialogs/skins/draw/BoxGeometry.cpp + # GUI - widgets ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h @@ -1084,20 +1115,14 @@ SET(LAUNCHER_SOURCES ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h - ui/widgets/DropLabel.cpp - ui/widgets/DropLabel.h - ui/widgets/FocusLineEdit.cpp - ui/widgets/FocusLineEdit.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h - ui/widgets/JavaSettingsWidget.cpp - ui/widgets/JavaSettingsWidget.h + ui/widgets/JavaWizardWidget.cpp + ui/widgets/JavaWizardWidget.h ui/widgets/LabeledToolButton.cpp ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp ui/widgets/LanguageSelectionWidget.h - ui/widgets/LineSeparator.cpp - ui/widgets/LineSeparator.h ui/widgets/LogView.cpp ui/widgets/LogView.h ui/widgets/InfoFrame.cpp @@ -1125,8 +1150,12 @@ SET(LAUNCHER_SOURCES ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp - ui/widgets/ThemeCustomizationWidget.h - ui/widgets/ThemeCustomizationWidget.cpp + ui/widgets/AppearanceWidget.h + ui/widgets/AppearanceWidget.cpp + ui/widgets/MinecraftSettingsWidget.h + ui/widgets/MinecraftSettingsWidget.cpp + ui/widgets/JavaSettingsWidget.h + ui/widgets/JavaSettingsWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp @@ -1142,8 +1171,15 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) +if (APPLE) + set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + ui/themes/ThemeManager.mm + ) +endif() + if (NOT Apple) -set(LAUNCHER_SOURCES + set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/dialogs/UpdateAvailableDialog.h @@ -1151,10 +1187,19 @@ set(LAUNCHER_SOURCES ) endif() +if (APPLE) + set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + + macsandbox/SecurityBookmarkFileAccess.h + macsandbox/SecurityBookmarkFileAccess.mm + ) +endif() + if(WIN32) set(LAUNCHER_SOURCES - WindowsConsole.cpp - WindowsConsole.h + console/WindowsConsole.h + console/WindowsConsole.cpp ${LAUNCHER_SOURCES} ) endif() @@ -1164,21 +1209,17 @@ qt_wrap_ui(LAUNCHER_UI ui/setupwizard/PasteWizardPage.ui ui/setupwizard/AutoJavaWizardPage.ui ui/setupwizard/LoginWizardPage.ui - ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui - ui/pages/global/MinecraftPage.ui ui/pages/global/ExternalToolsPage.ui ui/pages/instance/ExternalResourcesPage.ui ui/pages/instance/NotesPage.ui ui/pages/instance/LogPage.ui ui/pages/instance/ServersPage.ui - ui/pages/instance/GameOptionsPage.ui ui/pages/instance/OtherLogsPage.ui - ui/pages/instance/InstanceSettingsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui @@ -1194,14 +1235,16 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui - ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/EnvironmentVariables.ui ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui ui/widgets/SubTaskProgressBar.ui - ui/widgets/ThemeCustomizationWidget.ui + ui/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 @@ -1214,14 +1257,13 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui - ui/dialogs/OfflineLoginDialog.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 @@ -1244,8 +1286,10 @@ 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 - ../${Launcher_Branding_LogoQRC} + resources/shaders/shaders.qrc + "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" ) qt_wrap_ui(PRISMUPDATER_UI @@ -1261,6 +1305,19 @@ endif() include(CompilerWarnings) +######## Precompiled Headers ########### + +if(${Launcher_USE_PCH}) + message(STATUS "Using precompiled headers for applicable launcher targets") + set(PRECOMPILED_HEADERS + include/base.pch.hpp + include/qtcore.pch.hpp + include/qtgui.pch.hpp + ) +endif() + +####### Targets ######## + # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) set_project_warnings(Launcher_logic @@ -1269,19 +1326,40 @@ set_project_warnings(Launcher_logic "${Launcher_GCC_WARNINGS}") target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) + +if(${Launcher_USE_PCH}) + target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) +endif() + target_link_libraries(Launcher_logic systeminfo Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - tomlplusplus::tomlplusplus qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets - ghcFilesystem::ghc_filesystem ) -if (UNIX AND NOT CYGWIN AND NOT APPLE) +if(TARGET PkgConfig::libqrencode) + target_link_libraries(Launcher_logic PkgConfig::libqrencode) +else() + target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR}) + target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES}) +endif() + +if(TARGET PkgConfig::tomlplusplus) + target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) +else() + target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) +endif() +if(TARGET PkgConfig::libarchive) + target_link_libraries(Launcher_logic PkgConfig::libarchive) +else() + target_link_libraries(Launcher_logic LibArchive::LibArchive) +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(Launcher_logic gamemode ) @@ -1295,24 +1373,29 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth + Qt${QT_VERSION_MAJOR}::OpenGL + ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic - QuaZip::QuaZip cmark::cmark LocalPeer Launcher_rainbow ) +if (TARGET ${Launcher_QT_DBUS}) + add_compile_definitions(WITH_QTDBUS) +endif() + if(APPLE) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") if(Launcher_ENABLE_UPDATER) - file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) - file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) - find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") - add_compile_definitions(SPARKLE_ENABLED) + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + add_compile_definitions(SPARKLE_ENABLED) endif() target_link_libraries(Launcher_logic @@ -1322,13 +1405,18 @@ if(APPLE) "-framework ApplicationServices" ) if(Launcher_ENABLE_UPDATER) - target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) endif() endif() target_link_libraries(Launcher_logic) add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) + +if(${Launcher_USE_PCH}) + target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) +endif() + target_link_libraries(${Launcher_Name} Launcher_logic) if(DEFINED Launcher_APP_BINARY_NAME) @@ -1344,33 +1432,51 @@ if(DEFINED Launcher_APP_BINARY_DEFS) endif() install(TARGETS ${Launcher_Name} + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +# Deploy PDBs +if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) +endif() + if(Launcher_BUILD_UPDATER) # Updater add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + + if(${Launcher_USE_PCH}) + target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) + endif() + target_link_libraries(prism_updater_logic - QuaZip::QuaZip ${ZLIB_LIBRARIES} systeminfo BuildConfig - ghcFilesystem::ghc_filesystem Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network ${Launcher_QT_LIBS} cmark::cmark ) + if(TARGET PkgConfig::libarchive) + target_link_libraries(prism_updater_logic PkgConfig::libarchive) + else() + target_link_libraries(prism_updater_logic LibArchive::LibArchive) + endif() add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) + if(${Launcher_USE_PCH}) + target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) + endif() + if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") endif() @@ -1384,6 +1490,11 @@ if(Launcher_BUILD_UPDATER) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) + + # Deploy PDBs + if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) + endif() endif() if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) @@ -1395,10 +1506,14 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) "${Launcher_GCC_WARNINGS}") target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + + if(${Launcher_USE_PCH}) + target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) + endif() + target_link_libraries(filelink_logic systeminfo BuildConfig - ghcFilesystem::ghc_filesystem Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network @@ -1407,9 +1522,19 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) ) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) - target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + if(${Launcher_USE_PCH}) + target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) + endif() + + # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC + # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo + # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests + if(MSVC) + set_property(TARGET "${Launcher_Name}_filelink" PROPERTY LINK_FLAGS "/MANIFESTUAC:level='requireAdministrator'") + endif() + target_link_libraries("${Launcher_Name}_filelink" filelink_logic) if(DEFINED Launcher_APP_BINARY_NAME) @@ -1425,6 +1550,11 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) + + # Deploy PDBs + if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) + endif() endif() if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) @@ -1435,186 +1565,119 @@ if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) endif() #### The bundle mess! #### -# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. +# 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. -if(INSTALL_BUNDLE STREQUAL "full") - # Add qt.conf - this makes Qt stop looking for things outside the bundle - install( - CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" - COMPONENT Runtime - ) - # add qtlogging.ini as a config file - install( - FILES "qtlogging.ini" - DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} - COMPONENT Runtime - ) - # Bundle plugins - # Image formats - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE +if(WIN32 OR (UNIX AND APPLE)) + if(WIN32) + set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") + endif() + + qt_generate_deploy_script( + TARGET ${Launcher_Name} + OUTPUT_SCRIPT QT_DEPLOY_SCRIPT + CONTENT " + qt_deploy_runtime_dependencies( + EXECUTABLE ${BINARY_DEST_DIR}/$ + BIN_DIR ${BINARY_DEST_DIR} + LIBEXEC_DIR ${LIBRARY_DEST_DIR} + LIB_DIR ${LIBRARY_DEST_DIR} + PLUGINS_DIR ${PLUGIN_DEST_DIR} + NO_OVERWRITE + NO_TRANSLATIONS + NO_COMPILER_RUNTIME + DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} + )" ) - # Icon engines + + # Bundle our linked dependencies install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + COMPONENT bundle + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + # FIXME: Why aren't these caught by the below regex??? + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + POST_EXCLUDE_REGEXES + "system32" + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} + RUNTIME DESTINATION ${BINARY_DEST_DIR} + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} ) + # Deploy Qt plugins install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE + SCRIPT ${QT_DEPLOY_SCRIPT} + COMPONENT bundle ) - # Platform plugins + # FIXME: remove this crap once we stop using msys2 + if(MINGW) + # i've not found a solution better than injecting the config vars like this... + # with install(CODE" for everything everything just breaks + install(CODE " + set(QT_PLUGINS_DIR \"${QT_PLUGINS_DIR}\") + set(QT_LIBS_DIR \"${QT_LIBS_DIR}\") + set(QT_LIBEXECS_DIR \"${QT_LIBEXECS_DIR}\") + set(CMAKE_SYSTEM_LIBRARY_PATH \"${CMAKE_SYSTEM_LIBRARY_PATH}\") + set(CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\") + " + COMPONENT bundle) + + install(CODE [[ + file(GLOB QT_IMAGEFORMAT_DLLS "${QT_PLUGINS_DIR}/imageformats/*.dll") + set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL objdump) + file(GET_RUNTIME_DEPENDENCIES + RESOLVED_DEPENDENCIES_VAR imageformatdeps + LIBRARIES ${QT_IMAGEFORMAT_DLLS} + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_PLUGINS_DIR} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + # FIXME: Why aren't these caught by the below regex??? + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + POST_EXCLUDE_REGEXES + "system32" + ) + foreach(_lib ${imageformatdeps}) + file(INSTALL + DESTINATION ${CMAKE_INSTALL_PREFIX} + TYPE SHARED_LIBRARY + FOLLOW_SYMLINK_CHAIN + FILES ${_lib} + ) + endforeach() + ]] + COMPONENT bundle) + endif() + + # Add qt.conf - this makes Qt stop looking for things outside the bundle install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE + CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" + COMPONENT bundle ) + # Add qtlogging.ini as a config file install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE - REGEX "[^2]d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE + FILES "qtlogging.ini" + DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} + COMPONENT bundle ) - # Style plugins - if(EXISTS "${QT_PLUGINS_DIR}/styles") - install( - DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - # TLS plugins (Qt 6 only) - if(EXISTS "${QT_PLUGINS_DIR}/tls") - install( - DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - PATTERN "*qcertonlybackend*" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - PATTERN "*qcertonlybackend*" EXCLUDE - ) - endif() - # Wayland support - if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" - @ONLY +endif() + +find_program(CLANG_FORMAT clang-format OPTIONAL) +if(CLANG_FORMAT) + message(STATUS "Creating clang-format target") + add_custom_target( + clang-format + COMMAND ${CLANG_FORMAT} -i --style=file:${CMAKE_SOURCE_DIR}/.clang-format ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${PRISMUPDATER_SOURCES} ${LINKEXE_SOURCES} ${PRECOMPILED_HEADERS} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) - install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) +else() + message(WARNING "Unable to find `clang-format`. Not creating custom target") endif() diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 4ffa26fd4..9677f868e 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -12,10 +12,10 @@ #include -DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { - m_copy.matcher(m_pathMatcher.get()).whitelist(true); + m_copy.matcher(m_pathMatcher).whitelist(true); } void DataMigrationTask::executeTask() @@ -24,7 +24,7 @@ void DataMigrationTask::executeTask() // 1. Scan // Check how many files we gotta copy - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(true); // dry run to collect amount of files }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); @@ -37,11 +37,7 @@ void DataMigrationTask::dryRunFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Failed to scan source path.")); return; } @@ -57,7 +53,7 @@ void DataMigrationTask::dryRunFinished() setProgress(m_copy.totalCopied(), m_toCopy); setStatus(tr("Copying %1…").arg(shortenedName)); }); - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(false); // actually copy now }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); @@ -75,11 +71,7 @@ void DataMigrationTask::copyFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Some paths could not be copied!")); return; } diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h index fc613cd5e..9a2b0adb8 100644 --- a/launcher/DataMigrationTask.h +++ b/launcher/DataMigrationTask.h @@ -5,7 +5,7 @@ #pragma once #include "FileSystem.h" -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" #include "tasks/Task.h" #include @@ -18,7 +18,7 @@ class DataMigrationTask : public Task { Q_OBJECT public: - explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); ~DataMigrationTask() override = default; protected: @@ -33,7 +33,7 @@ class DataMigrationTask : public Task { private: const QString& m_sourcePath; const QString& m_targetPath; - const IPathMatcher::Ptr m_pathMatcher; + const Filter m_pathMatcher; FS::copy m_copy; int m_toCopy = 0; diff --git a/launcher/FastFileIconProvider.cpp b/launcher/FastFileIconProvider.cpp index f2b6f4425..1dbab27ba 100644 --- a/launcher/FastFileIconProvider.cpp +++ b/launcher/FastFileIconProvider.cpp @@ -44,4 +44,4 @@ QIcon FastFileIconProvider::icon(const QFileInfo& info) const } return QApplication::style()->standardIcon(icon); -} \ No newline at end of file +} diff --git a/launcher/FastFileIconProvider.h b/launcher/FastFileIconProvider.h index 208534044..7799b7879 100644 --- a/launcher/FastFileIconProvider.h +++ b/launcher/FastFileIconProvider.h @@ -23,4 +23,4 @@ class FastFileIconProvider : public QFileIconProvider { public: QIcon icon(const QFileInfo& info) const override; -}; \ No newline at end of file +}; diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index df06c3c75..445c2a881 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -40,12 +40,11 @@ #include #include #include -#include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" -FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), root(root) {} +FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {} // NOTE: Sadly, we have to do sorting ourselves. bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const { @@ -104,10 +103,10 @@ QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const if (index.column() == 0 && role == Qt::CheckStateRole) { QFileSystemModel* fsm = qobject_cast(sourceModel()); auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto cover = blocked.cover(blockedPath); + auto cover = m_blocked.cover(blockedPath); if (!cover.isNull()) { return QVariant(Qt::Unchecked); - } else if (blocked.exists(blockedPath)) { + } else if (m_blocked.exists(blockedPath)) { return QVariant(Qt::PartiallyChecked); } else { return QVariant(Qt::Checked); @@ -130,7 +129,7 @@ bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, i QString FileIgnoreProxy::relPath(const QString& path) const { - return QDir(root).relativeFilePath(path); + return QDir(m_root).relativeFilePath(path); } bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) @@ -146,18 +145,18 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) bool changed = false; if (state == Qt::Unchecked) { // blocking a path - auto& node = blocked.insert(blockedPath); + auto& node = m_blocked.insert(blockedPath); // get rid of all blocked nodes below node.clear(); changed = true; } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { - if (!blocked.remove(blockedPath)) { - auto cover = blocked.cover(blockedPath); + if (!m_blocked.remove(blockedPath)) { + auto cover = m_blocked.cover(blockedPath); qDebug() << "Blocked by cover" << cover; // uncover - blocked.remove(cover); + m_blocked.remove(cover); // block all contents, except for any cover - QModelIndex rootIndex = fsm->index(FS::PathCombine(root, cover)); + QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover)); QModelIndex doing = rootIndex; int row = 0; QStack todo; @@ -179,7 +178,7 @@ bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) todo.push(node); } else { // or just block this one. - blocked.insert(relpath); + m_blocked.insert(relpath); } row++; } @@ -229,7 +228,7 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) return false; } auto blockedPath = relPath(fsm->filePath(sourceIndex)); - auto found = blocked.find(blockedPath); + auto found = m_blocked.find(blockedPath); if (found) { return !found->leaf(); } @@ -239,8 +238,8 @@ bool FileIgnoreProxy::shouldExpand(QModelIndex index) void FileIgnoreProxy::setBlockedPaths(QStringList paths) { beginResetModel(); - blocked.clear(); - blocked.insert(paths); + m_blocked.clear(); + m_blocked.insert(paths); endResetModel(); } @@ -267,10 +266,45 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { - return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); + if (m_ignoreFiles.contains(fileInfo.fileName())) { + return true; + } + + for (const auto& suffix : m_ignoreFilesSuffixes) { + if (fileInfo.fileName().endsWith(suffix)) { + return true; + } + } + + if (m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()))) { + return true; + } + + return false; } -bool FileIgnoreProxy::filterFile(const QString& fileName) const +bool FileIgnoreProxy::filterFile(const QFileInfo& file) const { - return blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(root), fileName)); + return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); +} + +void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) +{ + QFile ignoreFile(fileName); + if (!ignoreFile.open(QIODevice::ReadOnly)) { + return; + } + auto ignoreData = ignoreFile.readAll(); + auto string = QString::fromUtf8(ignoreData); + setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); +} + +void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) +{ + auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8(); + try { + FS::write(fileName, ignoreData); + } catch (const Exception& e) { + qWarning() << e.cause(); + } } diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index e01a2651e..0f149ecb6 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -61,15 +61,20 @@ class FileIgnoreProxy : public QSortFilterProxyModel { void setBlockedPaths(QStringList paths); - inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return blocked; } - inline SeparatorPrefixTree<'/'>& blockedPaths() { return blocked; } + inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; } + inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; } // list of file names that need to be removed completely from model inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } + inline QStringList& ignoreFilesWithSuffix() { return m_ignoreFilesSuffixes; } // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } - bool filterFile(const QString& fileName) const; + bool filterFile(const QFileInfo& fileName) const; + + void loadBlockedPathsFromFile(const QString& fileName); + + void saveBlockedPathsToFile(const QString& fileName); protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; @@ -78,8 +83,9 @@ class FileIgnoreProxy : public QSortFilterProxyModel { bool ignoreFile(QFileInfo file) const; private: - const QString root; - SeparatorPrefixTree<'/'> blocked; + const QString m_root; + SeparatorPrefixTree<'/'> m_blocked; QStringList m_ignoreFiles; + QStringList m_ignoreFilesSuffixes; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 099ac2504..30d0a9c4c 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -59,10 +59,8 @@ #if defined Q_OS_WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN -#include #include #include -#include #include #include #include @@ -77,24 +75,8 @@ #include #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif // clone #if defined(Q_OS_LINUX) @@ -136,7 +118,6 @@ struct _DUPLICATE_EXTENTS_DATA { using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; - #endif struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { @@ -347,8 +328,8 @@ bool copy::operator()(const QString& offset, bool dryRun) opt |= copy_opts::overwrite_existing; // Function that'll do the actual copying - auto copy_file = [&](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) + auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); @@ -434,8 +415,8 @@ void create_link::make_link_list(const QString& offset) m_recursive = true; // Function that'll do the actual linking - auto link_file = [&](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { + auto link_file = [this, dst](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; } @@ -529,7 +510,7 @@ void create_link::runPrivileged(const QString& offset) QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); - connect(&m_linkServer, &QLocalServer::newConnection, this, [&]() { + connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() { qDebug() << "Client connected, sending out pairs"; // construct block of data to send QByteArray block; @@ -611,7 +592,7 @@ void create_link::runPrivileged(const QString& offset) } ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); - connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [&]() { emit finishedPrivileged(gotResults); }); + connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, gotResults]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); @@ -701,9 +682,6 @@ bool deletePath(QString path) bool trash(QString path, QString* pathInTrash) { -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - return false; -#else // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; @@ -712,7 +690,6 @@ bool trash(QString path, QString* pathInTrash) return false; #endif return QFile::moveToTrash(path, pathInTrash); -#endif } QString PathCombine(const QString& path1, const QString& path2) @@ -746,11 +723,7 @@ int pathDepth(const QString& path) QFileInfo info(path); -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); -#endif int numParts = parts.length(); numParts -= parts.count("."); @@ -770,11 +743,7 @@ QString pathTruncate(const QString& path, int depth) return pathTruncate(trunc, depth); } -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); -#endif if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); @@ -921,46 +890,65 @@ QString getDesktopDir() return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } +QString getApplicationsDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); +} + +QString quoteArgs(const QStringList& args, const QString& wrap, const QString& escapeChar, bool wrapOnlyIfNeeded = false) +{ + QString result; + + auto size = args.size(); + for (int i = 0; i < size; ++i) { + QString arg = args[i]; + arg.replace(wrap, escapeChar); + + bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); + + if (needsWrapping) + result += wrap + arg + wrap; + else + result += arg; + + if (i < size - 1) + result += ' '; + } + + return result; +} + // Cross-platform Shortcut creation -bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) +QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; - return false; + return QString(); } #if defined(Q_OS_MACOS) - // Create the Application - QDir applicationDirectory = - QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/"; - - if (!applicationDirectory.mkpath(".")) { - qWarning() << "Couldn't create application directory"; - return false; - } - - QDir application = applicationDirectory.path() + "/" + name + ".app/"; + QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; - return false; + return QString(); } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; - return false; + return QString(); } QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; - QFile info = content.path() + "/Info.plist"; + QFile info(content.path() + "/Info.plist"); if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; - return false; + return QString(); } info.open(QIODevice::WriteOnly | QIODevice::Text); @@ -973,9 +961,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); - QString argstring; - if (!args.empty()) - argstring = " \"" + args.join("\" \"") + "\""; + auto argstring = quoteArgs(args, "\"", "\\\""); stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; @@ -1009,22 +995,23 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri "\n" ""; - return true; + return application.path(); #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated destination += ".desktop"; QFile f(destination); - f.open(QIODevice::WriteOnly | QIODevice::Text); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { + qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; + return QString(); + } QTextStream stream(&f); - QString argstring; - if (!args.empty()) - argstring = " '" + args.join("' '") + "'"; + auto argstring = quoteArgs(args, "'", "'\\''"); stream << "[Desktop Entry]" << "\n"; stream << "Type=Application" << "\n"; stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; - stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; + stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; @@ -1035,51 +1022,38 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); - return true; + return destination; #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; - return false; + return QString(); } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; - return false; + return QString(); } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; - return false; + return QString(); } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; - return false; - } - - QString argStr; - int argCount = args.count(); - for (int i = 0; i < argCount; i++) { - if (args[i].contains(' ')) { - argStr.append('"').append(args[i]).append('"'); - } else { - argStr.append(args[i]); - } - - if (i < argCount - 1) { - argStr.append(" "); - } + return QString(); } + auto argStr = quoteArgs(args, "\"", "\\\"", true); if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; - return false; + return QString(); } HRESULT hres; @@ -1088,7 +1062,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; - return false; + return QString(); } WCHAR wsz[MAX_PATH]; @@ -1142,10 +1116,12 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri // go away COM, nobody likes you CoUninitialize(); - return SUCCEEDED(hres); + if (SUCCEEDED(hres)) + return destination; + return QString(); #else qWarning("Desktop Shortcuts not supported on your platform!"); - return false; + return QString(); #endif } @@ -1301,8 +1277,8 @@ bool clone::operator()(const QString& offset, bool dryRun) std::error_code err; // Function that'll do the actual cloneing - auto cloneFile = [&](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) + auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); @@ -1726,4 +1702,14 @@ QString getUniqueResourceName(const QString& filePath) return newFileName; } +bool removeFiles(QStringList listFile) +{ + bool ret = true; + // For each file + for (int i = 0; i < listFile.count(); i++) { + // Remove + ret = ret && QFile::remove(listFile.at(i)); + } + return ret; +} } // namespace FS diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index c5beef7bd..f2676b147 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -38,7 +38,7 @@ #pragma once #include "Exception.h" -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" #include @@ -115,9 +115,9 @@ class copy : public QObject { m_followSymlinks = follow; return *this; } - copy& matcher(const IPathMatcher* filter) + copy& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } copy& whitelist(bool whitelist) @@ -147,7 +147,7 @@ class copy : public QObject { private: bool m_followSymlinks = true; - const IPathMatcher* m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; bool m_overwrite = false; QDir m_src; @@ -209,9 +209,9 @@ class create_link : public QObject { m_useHardLinks = useHard; return *this; } - create_link& matcher(const IPathMatcher* filter) + create_link& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } create_link& whitelist(bool whitelist) @@ -260,7 +260,7 @@ class create_link : public QObject { private: bool m_useHardLinks = false; - const IPathMatcher* m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; bool m_recursive = true; @@ -291,6 +291,8 @@ bool move(const QString& source, const QString& dest); */ bool deletePath(QString path); +bool removeFiles(QStringList listFile); + /** * Trash a folder / file */ @@ -353,14 +355,18 @@ bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop QString getDesktopDir(); +// Get the Directory representing the User's Applications directory +QString getApplicationsDir(); + // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides bool overrideFolder(QString overwritten_path, QString override_path); /** * Creates a shortcut to the specified target file at the specified destination path. + * Returns null QString if creation failed; otherwise returns the path to the created shortcut. */ -bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); +QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); enum class FilesystemType { FAT, @@ -488,9 +494,9 @@ class clone : public QObject { m_src.setPath(src); m_dst.setPath(dst); } - clone& matcher(const IPathMatcher* filter) + clone& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } clone& whitelist(bool whitelist) @@ -514,7 +520,7 @@ class clone : public QObject { bool operator()(const QString& offset, bool dryRun = false); private: - const IPathMatcher* m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; QDir m_src; QDir m_dst; diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp deleted file mode 100644 index adeb2209e..000000000 --- a/launcher/Filter.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "Filter.h" - -ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern) {} -bool ContainsFilter::accepts(const QString& value) -{ - return value.contains(pattern); -} - -ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern) {} -bool ExactFilter::accepts(const QString& value) -{ - return value == pattern; -} - -ExactIfPresentFilter::ExactIfPresentFilter(const QString& pattern) : pattern(pattern) {} -bool ExactIfPresentFilter::accepts(const QString& value) -{ - return value.isEmpty() || value == pattern; -} - -RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) -{ - pattern.setPattern(regexp); - pattern.optimize(); -} -bool RegexpFilter::accepts(const QString& value) -{ - auto match = pattern.match(value); - bool matched = match.hasMatch(); - return invert ? (!matched) : (matched); -} - -ExactListFilter::ExactListFilter(const QStringList& pattern) : m_pattern(pattern) {} -bool ExactListFilter::accepts(const QString& value) -{ - return m_pattern.isEmpty() || m_pattern.contains(value); -} \ No newline at end of file diff --git a/launcher/Filter.h b/launcher/Filter.h index ae835e724..317f5b067 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -3,59 +3,52 @@ #include #include -class Filter { - public: - virtual ~Filter() = default; - virtual bool accepts(const QString& value) = 0; -}; - -class ContainsFilter : public Filter { - public: - ContainsFilter(const QString& pattern); - virtual ~ContainsFilter() = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class ExactFilter : public Filter { - public: - ExactFilter(const QString& pattern); - virtual ~ExactFilter() = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class ExactIfPresentFilter : public Filter { - public: - ExactIfPresentFilter(const QString& pattern); - virtual ~ExactIfPresentFilter() override = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class RegexpFilter : public Filter { - public: - RegexpFilter(const QString& regexp, bool invert); - virtual ~RegexpFilter() = default; - bool accepts(const QString& value) override; - - private: - QRegularExpression pattern; - bool invert = false; -}; - -class ExactListFilter : public Filter { - public: - ExactListFilter(const QStringList& pattern = {}); - virtual ~ExactListFilter() = default; - bool accepts(const QString& value) override; - - private: - QStringList m_pattern; -}; +using Filter = std::function; + +namespace Filters { +inline Filter inverse(Filter filter) +{ + return [filter = std::move(filter)](const QString& src) { return !filter(src); }; +} + +inline Filter any(QList filters) +{ + return [filters = std::move(filters)](const QString& src) { + for (auto& filter : filters) + if (filter(src)) + return true; + + return false; + }; +} + +inline Filter equals(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; +} + +inline Filter equalsAny(QStringList patterns = {}) +{ + return [patterns = std::move(patterns)](const QString& src) { return patterns.isEmpty() || patterns.contains(src); }; +} + +inline Filter equalsOrEmpty(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; +} + +inline Filter contains(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; +} + +inline Filter startsWith(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; +} + +inline Filter regexp(QRegularExpression pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; +} +} // namespace Filters diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 1c2539e08..dc786e10e 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -36,6 +36,8 @@ #include "GZip.h" #include #include +#include +#include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { @@ -136,3 +138,81 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) } return true; } + +int inf(QFile* source, std::function handleBlock) +{ + constexpr auto CHUNK = 16384; + int ret; + unsigned have; + z_stream strm; + memset(&strm, 0, sizeof(strm)); + char in[CHUNK]; + unsigned char out[CHUNK]; + + ret = inflateInit2(&strm, (16 + MAX_WBITS)); + if (ret != Z_OK) + return ret; + + /* decompress until deflate stream ends or end of file */ + do { + strm.avail_in = source->read(in, CHUNK); + if (source->error()) { + (void)inflateEnd(&strm); + return Z_ERRNO; + } + if (strm.avail_in == 0) + break; + strm.next_in = reinterpret_cast(in); + + /* run inflate() on input until output buffer not full */ + do { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate(&strm, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); /* state not clobbered */ + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; /* and fall through */ + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void)inflateEnd(&strm); + return ret; + } + have = CHUNK - strm.avail_out; + if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { + (void)inflateEnd(&strm); + return Z_OK; + } + + } while (strm.avail_out == 0); + + /* done when inflate() says it's done */ + } while (ret != Z_STREAM_END); + + /* clean up and return */ + (void)inflateEnd(&strm); + return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; +} + +QString zerr(int ret) +{ + switch (ret) { + case Z_ERRNO: + return QObject::tr("error handling file"); + case Z_STREAM_ERROR: + return QObject::tr("invalid compression level"); + case Z_DATA_ERROR: + return QObject::tr("invalid or incomplete deflate data"); + case Z_MEM_ERROR: + return QObject::tr("out of memory"); + case Z_VERSION_ERROR: + return QObject::tr("zlib version mismatch!"); + } + return {}; +} + +QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) +{ + auto ret = inf(source, handleBlock); + return zerr(ret); +} diff --git a/launcher/GZip.h b/launcher/GZip.h index 0bdb70407..b736ca93f 100644 --- a/launcher/GZip.h +++ b/launcher/GZip.h @@ -1,8 +1,11 @@ #pragma once #include +#include -class GZip { - public: - static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); - static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); -}; +namespace GZip { + +bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); +bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); +QString readGzFileByBlocks(QFile* source, std::function handleBlock); + +} // namespace GZip diff --git a/launcher/InstanceCopyPrefs.cpp b/launcher/InstanceCopyPrefs.cpp index 63c200cc4..087b37340 100644 --- a/launcher/InstanceCopyPrefs.cpp +++ b/launcher/InstanceCopyPrefs.cpp @@ -189,4 +189,4 @@ void InstanceCopyPrefs::enableDontLinkSaves(bool b) void InstanceCopyPrefs::enableUseClone(bool b) { useClone = b; -} \ No newline at end of file +} diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 61c51b3b7..1c3c0c984 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -8,23 +8,23 @@ struct InstanceCopyPrefs { public: - [[nodiscard]] bool allTrue() const; - [[nodiscard]] QString getSelectedFiltersAsRegex() const; - [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; + bool allTrue() const; + QString getSelectedFiltersAsRegex() const; + QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters - [[nodiscard]] bool isCopySavesEnabled() const; - [[nodiscard]] bool isKeepPlaytimeEnabled() const; - [[nodiscard]] bool isCopyGameOptionsEnabled() const; - [[nodiscard]] bool isCopyResourcePacksEnabled() const; - [[nodiscard]] bool isCopyShaderPacksEnabled() const; - [[nodiscard]] bool isCopyServersEnabled() const; - [[nodiscard]] bool isCopyModsEnabled() const; - [[nodiscard]] bool isCopyScreenshotsEnabled() const; - [[nodiscard]] bool isUseSymLinksEnabled() const; - [[nodiscard]] bool isLinkRecursivelyEnabled() const; - [[nodiscard]] bool isUseHardLinksEnabled() const; - [[nodiscard]] bool isDontLinkSavesEnabled() const; - [[nodiscard]] bool isUseCloneEnabled() const; + bool isCopySavesEnabled() const; + bool isKeepPlaytimeEnabled() const; + bool isCopyGameOptionsEnabled() const; + bool isCopyResourcePacksEnabled() const; + bool isCopyShaderPacksEnabled() const; + bool isCopyServersEnabled() const; + bool isCopyModsEnabled() const; + bool isCopyScreenshotsEnabled() const; + bool isUseSymLinksEnabled() const; + bool isLinkRecursivelyEnabled() const; + bool isUseHardLinksEnabled() const; + bool isDontLinkSavesEnabled() const; + bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index 0220a4144..e2c7020a3 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -3,12 +3,12 @@ #include #include #include "FileSystem.h" +#include "Filter.h" #include "NullInstance.h" -#include "pathmatcher/RegexpMatcher.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" -InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) +InstanceCopyTask::InstanceCopyTask(BaseInstance* origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); @@ -30,9 +30,8 @@ InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyP if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... - auto matcherReal = new RegexpMatcher(filters); - matcherReal->caseSensitive(false); - m_matcher.reset(matcherReal); + QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); + m_matcher = Filters::regexp(regexp); } } @@ -43,7 +42,7 @@ void InstanceCopyTask::executeTask() m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); - folderClone.matcher(m_matcher.get()); + folderClone.matcher(m_matcher); folderClone(true); setProgress(0, folderClone.totalCloned()); @@ -72,7 +71,7 @@ void InstanceCopyTask::executeTask() } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder - folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher.get()); + folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); folderLink(true); setProgress(0, m_progressTotal + folderLink.totalToLink()); @@ -91,7 +90,7 @@ void InstanceCopyTask::executeTask() QEventLoop loop; bool got_priv_results = false; - connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { + connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) { if (!gotResults) { qDebug() << "Privileged run exited without results!"; } @@ -127,7 +126,7 @@ void InstanceCopyTask::executeTask() return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).matcher(m_matcher.get()); + folderCopy.followSymlinks(false).matcher(m_matcher); folderCopy(true); setProgress(0, folderCopy.totalCopied()); @@ -148,9 +147,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) { @@ -198,4 +197,4 @@ bool InstanceCopyTask::abort() return true; } return false; -} \ No newline at end of file +} diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 0f7f1020d..a926af8a7 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -5,6 +5,7 @@ #include #include "BaseInstance.h" #include "BaseVersion.h" +#include "Filter.h" #include "InstanceCopyPrefs.h" #include "InstanceTask.h" #include "net/NetJob.h" @@ -14,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. @@ -25,10 +26,10 @@ class InstanceCopyTask : public InstanceTask { private: /* data */ - InstancePtr m_origInstance; + BaseInstance* m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; - std::unique_ptr m_matcher; + Filter m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; diff --git a/launcher/InstanceDirUpdate.cpp b/launcher/InstanceDirUpdate.cpp new file mode 100644 index 000000000..75fbdb6c6 --- /dev/null +++ b/launcher/InstanceDirUpdate.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceDirUpdate.h" + +#include + +#include "Application.h" +#include "FileSystem.h" + +#include "InstanceList.h" +#include "ui/dialogs/CustomMessageBox.h" + +QString askToUpdateInstanceDirName(BaseInstance* instance, const QString& oldName, const QString& newName, QWidget* parent) +{ + if (oldName == newName) + return QString(); + + QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); + if (renamingMode == "MetadataOnly") + return QString(); + + auto oldRoot = instance->instanceRoot(); + auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); + auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); + if (oldRoot == newRoot) + return QString(); + if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) + return QString(); + + // Check for conflict + if (QDir(newRoot).exists()) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("New instance root (%1) already exists.
Only the metadata will be renamed.").arg(newRoot)); + return QString(); + } + + // Ask if we should rename + if (renamingMode == "AskEverytime") { + auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); + auto dialog = + CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), + QObject::tr("Would you also like to rename the instance folder?\n\n" + "Old name: %1\n" + "New name: %2") + .arg(oldName, newName), + QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); + + auto res = dialog->exec(); + if (checkBox->isChecked()) { + if (res == QMessageBox::Yes) + APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); + else + APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); + } + if (res == QMessageBox::No) + return QString(); + } + + // Check for linked instances + if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) + return QString(); + + // Now we can confirm that a renaming is happening + if (!instance->syncInstanceDirName(newRoot)) { + QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), + QObject::tr("An error occurred when performing the following renaming operation:
" + " - Old instance root: %1
" + " - New instance root: %2
" + "Only the metadata is renamed.") + .arg(oldRoot, newRoot)); + return QString(); + } + return newRoot; +} + +bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) +{ + auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); + if (!linkedInstances.empty()) { + auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), + QObject::tr("The following instance(s) might reference files in this instance:\n\n" + "%1\n\n" + "%2 it could break the other instance(s), \n\n" + "Do you wish to proceed?", + nullptr, linkedInstances.count()) + .arg(linkedInstances.join("\n")) + .arg(verb), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return false; + } + return true; +} diff --git a/launcher/InstanceDirUpdate.h b/launcher/InstanceDirUpdate.h new file mode 100644 index 000000000..9da49a9a6 --- /dev/null +++ b/launcher/InstanceDirUpdate.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseInstance.h" + +/// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened +QString askToUpdateInstanceDirName(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 71630656d..44f513bc7 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -38,10 +38,11 @@ #include "Application.h" #include "FileSystem.h" -#include "MMCZip.h" #include "NullInstance.h" #include "QObjectPtr.h" +#include "archive/ArchiveReader.h" +#include "archive/ExtractZipTask.h" #include "icons/IconList.h" #include "icons/IconUtils.h" @@ -54,12 +55,10 @@ #include "net/ApiDownload.h" +#include #include -#include #include -#include - InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) {} @@ -72,7 +71,6 @@ bool InstanceImportTask::abort() bool wasAborted = false; if (m_task) wasAborted = m_task->abort(); - Task::abort(); return wasAborted; } @@ -110,39 +108,14 @@ void InstanceImportTask::downloadFromUrl() filesNetJob->start(); } -QString InstanceImportTask::getRootFromZip(QuaZip* zip, const QString& root) +QString cleanPath(QString path) { - if (!isRunning()) { - return {}; - } - QuaZipDir rootDir(zip, root); - for (auto&& fileName : rootDir.entryList(QDir::Files)) { - setDetails(fileName); - if (fileName == "instance.cfg") { - qDebug() << "MultiMC:" << true; - m_modpackType = ModpackType::MultiMC; - return root; - } - if (fileName == "manifest.json") { - qDebug() << "Flame:" << true; - m_modpackType = ModpackType::Flame; - return root; - } - - QCoreApplication::processEvents(); - } - - // Recurse the search to non-ignored subfolders - for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { - if ("overrides/" == fileName) - continue; - - QString result = getRootFromZip(zip, root + fileName); - if (!result.isEmpty()) - return result; - } - - return {}; + if (path == ".") + return QString(); + QString result = path; + if (result.startsWith("./")) + result = result.mid(2); + return result; } void InstanceImportTask::processZipPack() @@ -152,33 +125,47 @@ void InstanceImportTask::processZipPack() qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - auto packZip = std::make_shared(m_archivePath); - if (!packZip->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied modpack zip file.")); - return; - } - - QuaZipDir packZipDir(packZip.get()); + MMCZip::ArchiveReader packZip(m_archivePath); qDebug() << "Attempting to determine instance type"; QString root; - // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // https://docs.modrinth.com/docs/modpacks/format_definition/#storage - if (packZipDir.exists("/modrinth.index.json")) { - // process as Modrinth pack - qDebug() << "Modrinth:" << true; - m_modpackType = ModpackType::Modrinth; - } else if (packZipDir.exists("/bin/modpack.jar") || packZipDir.exists("/bin/version.json")) { - // process as Technic pack - qDebug() << "Technic:" << true; - extractDir.mkpath("minecraft"); - extractDir.cd("minecraft"); - m_modpackType = ModpackType::Technic; - } else { - root = getRootFromZip(packZip.get()); - setDetails(""); + auto detectInstance = [this, &extractDir, &root](MMCZip::ArchiveReader::File* f, bool& stop) { + if (!isRunning()) { + stop = true; + return true; + } + auto fileName = f->filename(); + if (fileName == "modrinth.index.json") { + // process as Modrinth pack + qDebug() << "Modrinth:" << true; + m_modpackType = ModpackType::Modrinth; + stop = true; + } else if (fileName == "bin/modpack.jar" || fileName == "bin/version.json") { + // process as Technic pack + qDebug() << "Technic:" << true; + extractDir.mkpath("minecraft"); + extractDir.cd("minecraft"); + m_modpackType = ModpackType::Technic; + stop = true; + } else if (fileName == "manifest.json") { + qDebug() << "Flame:" << true; + m_modpackType = ModpackType::Flame; + stop = true; + } else if (QFileInfo fileInfo(fileName); fileInfo.fileName() == "instance.cfg") { + qDebug() << "MultiMC:" << true; + m_modpackType = ModpackType::MultiMC; + root = cleanPath(fileInfo.path()); + stop = true; + } + QCoreApplication::processEvents(); + return true; + }; + if (!packZip.parse(detectInstance)) { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -187,7 +174,7 @@ void InstanceImportTask::processZipPack() setStatus(tr("Extracting modpack")); // make sure we extract just the pack - auto zipTask = makeShared(packZip, extractDir, root); + auto zipTask = makeShared(m_archivePath, extractDir, root); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { @@ -212,6 +199,7 @@ void InstanceImportTask::processZipPack() progressStep->status = status; stepProgress(*progressStep); }); + connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(zipTask); zipTask->start(); } @@ -263,6 +251,25 @@ void InstanceImportTask::extractFinished() } } +bool installIcon(QString root, QString instIconKey) +{ + auto importIconPath = IconUtils::findBestIconIn(root, instIconKey); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(root, "icon.png"); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(instIconKey)) { + iconList->deleteIcon(instIconKey); + } + iconList->installIcon(importIconPath, instIconKey + "." + QFileInfo(importIconPath).suffix()); + return true; + } + return false; +} + void InstanceImportTask::processFlame() { shared_qobject_ptr inst_creation_task = nullptr; @@ -288,6 +295,14 @@ void InstanceImportTask::processFlame() } inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") { + auto iconKey = QString("Flame_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) { + m_instIcon = iconKey; + } + } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); @@ -305,9 +320,11 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + 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::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); @@ -324,9 +341,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(); @@ -340,17 +357,7 @@ void InstanceImportTask::processMultiMC() } else { m_instIcon = instance.iconKey(); - auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); - if (importIconPath.isNull() || !QFile::exists(importIconPath)) - importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png"); - if (!importIconPath.isNull() && QFile::exists(importIconPath)) { - // import icon - auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(m_instIcon)) { - iconList->deleteIcon(m_instIcon); - } - iconList->installIcon(importIconPath, m_instIcon); - } + installIcon(instance.instanceRoot(), m_instIcon); } emitSucceeded(); } @@ -378,8 +385,8 @@ void InstanceImportTask::processModrinth() } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { - QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); - pack_id = regex.match(m_sourceUrl.toString()).captured(1); + static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); + pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs @@ -387,6 +394,14 @@ void InstanceImportTask::processModrinth() } inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") { + auto iconKey = QString("Modrinth_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) { + m_instIcon = iconKey; + } + } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); @@ -404,9 +419,11 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + 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::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 8884e0801..c92e229a0 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -40,8 +40,6 @@ #include #include "InstanceTask.h" -class QuaZip; - class InstanceImportTask : public InstanceTask { Q_OBJECT public: @@ -58,7 +56,6 @@ class InstanceImportTask : public InstanceTask { void processTechnic(); void processFlame(); void processModrinth(); - QString getRootFromZip(QuaZip* zip, const QString& root = ""); private slots: void processZipPack(); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index e1fa755dd..68681a9bb 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -60,6 +60,7 @@ #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" +#include "minecraft/ShortcutUtils.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 @@ -68,7 +69,7 @@ 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(); @@ -142,7 +143,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()); } @@ -152,15 +153,15 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return m_instances.count(); + return m_instances.size(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); - if (row < 0 || row >= m_instances.size()) + if (row < 0 || static_cast(row) >= m_instances.size()) 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 @@ -265,7 +266,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(); } @@ -333,7 +334,7 @@ bool InstanceList::trashInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; return false; } @@ -348,26 +349,43 @@ bool InstanceList::trashInstance(const InstanceId& id) } if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { - qDebug() << "Trash of instance" << id << "has not been completely successfully..."; + qWarning() << "Trash of instance" << id << "has not been completely successful..."; return false; } qDebug() << "Instance" << id << "has been trashed by the launcher."; m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); + // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway + for (const auto& [name, filePath, target] : inst->shortcuts()) { + if (!FS::trash(filePath, &trashedLoc)) { + qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, trying to delete it instead..."; + if (!FS::deletePath(filePath)) { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, given up..."; + } else { + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; + } + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; + m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); + } + return true; } -bool InstanceList::trashedSomething() +bool InstanceList::trashedSomething() const { return !m_trashHistory.empty(); } -void InstanceList::undoTrashInstance() +bool InstanceList::undoTrashInstance() { if (m_trashHistory.empty()) { qWarning() << "Nothing to recover from trash."; - return; + return true; } auto top = m_trashHistory.pop(); @@ -377,21 +395,41 @@ void InstanceList::undoTrashInstance() top.path += "1"; } + if (!QFile(top.trashPath).rename(top.path)) { + qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; + return false; + } qDebug() << "Moving" << top.trashPath << "back to" << top.path; - QFile(top.trashPath).rename(top.path); + + bool ok = true; + for (const auto& [data, trashPath] : top.shortcuts) { + if (QDir(data.filePath).exists()) { + // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it + qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; + ok = false; + continue; + } + if (!QFile(trashPath).rename(data.filePath)) { + qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; + ok = false; + continue; + } + qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; + } m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); + return ok; } void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; + qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } @@ -404,14 +442,22 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Will delete instance" << id; if (!FS::deletePath(inst->instanceRoot())) { - qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; + qWarning() << "Deletion of instance" << id << "has not been completely successful..."; return; } qDebug() << "Instance" << id << "has been deleted by the launcher."; + + for (const auto& [name, filePath, target] : inst->shortcuts()) { + if (!FS::deletePath(filePath)) { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; + } } -static QMap getIdMapping(const QList& list) +static QMap getIdMapping(const std::vector>& list) { QMap out; int i = 0; @@ -420,7 +466,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; @@ -428,7 +474,7 @@ static QMap getIdMapping(const QList& QList InstanceList::discoverInstances() { - qDebug() << "Discovering instances in" << m_instDir; + qInfo() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { @@ -447,13 +493,9 @@ QList InstanceList::discoverInstances() } auto id = dirInfo.fileName(); out.append(id); - qDebug() << "Found instance ID" << id; + qInfo() << "Found instance ID" << id; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) instanceSet = QSet(out.begin(), out.end()); -#else - instanceSet = out.toSet(); -#endif m_instancesProbed = true; return out; } @@ -462,17 +504,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); - qDebug() << "Should keep and soft-reload" << 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)); } } } @@ -487,7 +528,7 @@ InstanceList::InstListError InstanceList::loadList() int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [&]() { + auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); @@ -525,7 +566,7 @@ void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; for (auto const& itr : m_instances) { - totalPlayTime += itr.get()->totalTimePlayed(); + totalPlayTime += itr->totalTimePlayed(); } } @@ -536,12 +577,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(), m_instances.size(), m_instances.size() + 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(); } @@ -571,26 +612,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 {}; @@ -598,14 +639,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 = m_instances.size(); for (int i = 0; i < count; i++) { - if (inst == m_instances[i].get()) { + if (inst == m_instances.at(i).get()) { return i; } } @@ -621,15 +662,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", ""); @@ -638,11 +679,16 @@ 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(); + qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); + + auto shortcut = inst->shortcuts(); + if (!shortcut.isEmpty()) + qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); + return inst; } @@ -864,15 +910,14 @@ 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); @@ -948,7 +993,7 @@ class InstanceStaging : public Task { */ ExponentialSeries backoff; QString m_stagingPath; - unique_qobject_ptr m_child; + std::unique_ptr m_child; QTimer m_backoffTimer; }; @@ -989,7 +1034,6 @@ bool InstanceList::commitStagedInstance(const QString& path, groupName = QString(); QString instID; - InstancePtr inst; auto should_override = commiting.shouldOverride(); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index c85fe55c7..d996e0865 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -50,24 +50,30 @@ struct InstanceName; using InstanceId = QString; using GroupId = QString; -using InstanceLocator = std::pair; +using InstanceLocator = std::pair; enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateError, InstExists, CantCreateDir }; enum class GroupsState { NotLoaded, Steady, Dirty }; +struct TrashShortcutItem { + ShortcutData data; + QString trashPath; +}; + struct TrashHistoryItem { QString id; QString path; QString trashPath; QString groupName; + QList shortcuts; }; class InstanceList : public QAbstractListModel { Q_OBJECT public: - explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); + explicit InstanceList(SettingsObject* settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: @@ -90,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 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); @@ -111,8 +117,8 @@ class InstanceList : public QAbstractListModel { void deleteGroup(const GroupId& name); void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); - bool trashedSomething(); - void undoTrashInstance(); + bool trashedSomething() const; + bool undoTrashInstance(); void deleteInstance(const InstanceId& id); // Wrap an instance creation task in some more task machinery and make it ready to be used @@ -173,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); @@ -186,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 174041f89..134fb8f24 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" @@ -20,39 +21,36 @@ 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 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 GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); - values.append(new InstanceSettingsPage(onesix.get())); - auto logMatcher = inst->getLogFileMatcher(); - if (logMatcher) { - values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); - } + values.append(new InstanceSettingsPage(onesix)); + values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); return values; } virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: - InstancePtr inst; + BaseInstance* inst; }; diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 7c02160a7..cd5a2b35a 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -14,10 +14,10 @@ struct InstanceName { InstanceName() = default; InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} - [[nodiscard]] QString modifiedName() const; - [[nodiscard]] QString originalName() const; - [[nodiscard]] QString name() const; - [[nodiscard]] QString version() const; + QString modifiedName() const; + QString originalName() const; + QString name() const; + QString version() const; void setName(QString name) { m_modified_name = name; } void setName(InstanceName& other); @@ -35,7 +35,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; } @@ -44,12 +44,12 @@ class InstanceTask : public Task, public InstanceName { void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } - [[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; } + bool shouldConfirmUpdate() const { return m_confirm_update; } void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } bool shouldOverride() const { return m_override_existing; } - [[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; }; + QString originalInstanceID() const { return m_original_instance_id; }; protected: void setOverride(bool override, QString instance_id_to_override = {}) @@ -60,7 +60,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/JavaCommon.cpp b/launcher/JavaCommon.cpp index 188edb943..7bb674dde 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -41,7 +41,9 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { - if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") || + static const QRegularExpression s_memRegex("-Xm[sx]"); + static const QRegularExpression s_versionRegex("-version:.*"); + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " @@ -52,7 +54,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return false; } // block lunacy with passing required version to the JVM - if (jvmargs.contains(QRegularExpression("-version:.*"))) { + if (jvmargs.contains(s_versionRegex)) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" @@ -93,7 +95,7 @@ void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& re { QString text; text += QObject::tr( - "The specified Java binary didn't work.
You should use the auto-detect feature, " + "The specified Java binary didn't work.
You should press 'Detect', " "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index a21b5a494..0e4aa2b0a 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -24,7 +24,7 @@ class TestCheck : public QObject { TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} - virtual ~TestCheck() {}; + virtual ~TestCheck() = default; void run(); diff --git a/launcher/Json.cpp b/launcher/Json.cpp index f397f89c5..688f9dae7 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -101,6 +101,21 @@ QJsonArray requireArray(const QJsonDocument& doc, const QString& what) return doc.array(); } +QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error, QString* garbage) +{ + auto doc = QJsonDocument::fromJson(json, error); + if (error->error == QJsonParseError::GarbageAtEnd) { + qsizetype offset = error->offset; + QByteArray validJson = json.left(offset); + doc = QJsonDocument::fromJson(validJson, error); + + if (garbage) + *garbage = json.right(json.size() - offset); + } + + return doc; +} + void writeString(QJsonObject& to, const QString& key, const QString& value) { if (!value.isEmpty()) { @@ -153,7 +168,7 @@ QJsonValue toJson(const QVariant& variant) template <> QByteArray requireIsType(const QJsonValue& value, const QString& what) { - const QString string = ensureIsType(value, what); + const QString string = value.toString(what); // ensure that the string can be safely cast to Latin1 if (string != QString::fromLatin1(string.toLatin1())) { throw JsonException(what + " is not encodable as Latin1"); @@ -221,7 +236,7 @@ QDateTime requireIsType(const QJsonValue& value, const QString& what) template <> QUrl requireIsType(const QJsonValue& value, const QString& what) { - const QString string = ensureIsType(value, what); + const QString string = value.toString(what); if (string.isEmpty()) { return QUrl(); } @@ -279,4 +294,48 @@ QJsonValue requireIsType(const QJsonValue& value, const QString& wha return value; } +QStringList toStringList(const QString& jsonString) +{ + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isArray()) + return {}; + try { + return requireIsArrayOf(doc); + } catch (Json::JsonException& e) { + return {}; + } +} + +QString fromStringList(const QStringList& list) +{ + QJsonArray array; + for (const QString& str : list) { + array.append(str); + } + + QJsonDocument doc(toJsonArray(list)); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); +} + +QVariantMap toMap(const QString& jsonString) +{ + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + return {}; + + QJsonObject obj = doc.object(); + return obj.toVariantMap(); +} + +QString fromMap(const QVariantMap& map) +{ + QJsonObject obj = QJsonObject::fromVariantMap(map); + QJsonDocument doc(obj); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); +} + } // namespace Json diff --git a/launcher/Json.h b/launcher/Json.h index 28891f398..7a50af167 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -99,7 +99,7 @@ template QJsonArray toJsonArray(const QList& container) { QJsonArray array; - for (const T item : container) { + for (const T& item : container) { array.append(toJson(item)); } return array; @@ -107,6 +107,9 @@ QJsonArray toJsonArray(const QList& container) ////////////////// READING //////////////////// +// Attempt to parse JSON up until garbage is encountered +QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error = nullptr, QString* garbage = nullptr); + /// @throw JsonException template T requireIsType(const QJsonValue& value, const QString& what = "Value"); @@ -153,18 +156,6 @@ QUrl requireIsType(const QJsonValue& value, const QString& what); // the following functions are higher level functions, that make use of the above functions for // type conversion -template -T ensureIsType(const QJsonValue& value, const T default_ = T(), const QString& what = "Value") -{ - if (value.isUndefined() || value.isNull()) { - return default_; - } - try { - return requireIsType(value, what); - } catch (const JsonException&) { - return default_; - } -} /// @throw JsonException template @@ -178,68 +169,31 @@ T requireIsType(const QJsonObject& parent, const QString& key, const QString& wh } template -T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = T(), const QString& what = "__placeholder__") -{ - const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); - if (!parent.contains(key)) { - return default_; - } - return ensureIsType(parent.value(key), default_, localWhat); -} - -template -QVector requireIsArrayOf(const QJsonDocument& doc) +QList requireIsArrayOf(const QJsonDocument& doc) { const QJsonArray array = requireArray(doc); - QVector out; + QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } return out; } -template -QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") -{ - const QJsonArray array = ensureIsType(value, QJsonArray(), what); - QVector out; - for (const QJsonValue val : array) { - out.append(requireIsType(val, what)); - } - return out; -} - -template -QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value") -{ - if (value.isUndefined()) { - return default_; - } - return ensureIsArrayOf(value, what); -} - /// @throw JsonException template -QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") +QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { throw JsonException(localWhat + "s parent does not contain " + localWhat); } - return ensureIsArrayOf(parent.value(key), localWhat); -} -template -QVector ensureIsArrayOf(const QJsonObject& parent, - const QString& key, - const QVector& default_ = QVector(), - const QString& what = "__placeholder__") -{ - const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); - if (!parent.contains(key)) { - return default_; + const QJsonArray array = parent[key].toArray(); + QList out; + for (const QJsonValue val : array) { + out.append(requireIsType(val, "Document")); } - return ensureIsArrayOf(parent.value(key), default_, localWhat); + return out; } // this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers @@ -248,18 +202,9 @@ QVector ensureIsArrayOf(const QJsonObject& parent, { \ return requireIsType(value, what); \ } \ - inline TYPE ensure##NAME(const QJsonValue& value, const TYPE default_ = TYPE(), const QString& what = "Value") \ - { \ - return ensureIsType(value, default_, what); \ - } \ inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \ { \ return requireIsType(parent, key, what); \ - } \ - inline TYPE ensure##NAME(const QJsonObject& parent, const QString& key, const TYPE default_ = TYPE(), \ - const QString& what = "__placeholder") \ - { \ - return ensureIsType(parent, key, default_, what); \ } JSON_HELPERFUNCTIONS(Array, QJsonArray) @@ -278,5 +223,12 @@ JSON_HELPERFUNCTIONS(Variant, QVariant) #undef JSON_HELPERFUNCTIONS +// helper functions for settings +QStringList toStringList(const QString& jsonString); +QString fromStringList(const QStringList& list); + +QVariantMap toMap(const QString& jsonString); +QString fromMap(const QVariantMap& map); + } // namespace Json using JSONValidationError = Json::JsonException; diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 5d1cebd50..1c11208ac 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -43,6 +43,7 @@ #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" @@ -60,6 +61,7 @@ #include "JavaCommon.h" #include "launch/steps/TextPrint.h" #include "tasks/Task.h" +#include "ui/dialogs/ChooseOfflineNameDialog.h" LaunchController::LaunchController() : Task() {} @@ -130,107 +132,166 @@ void LaunchController::decideAccount() } void LaunchController::login() + { + decideAccount(); + + // if no account is selected, we bail + if (!m_accountToUse) { + 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; + + while (tryagain) { + if (tries > 0 && tries % 3 == 0) { + auto result = + QMessageBox::question(m_parentWidget, tr("Continue launch?"), + tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?").arg(tries)); + + if (result == QMessageBox::No) { + emitAborted(); + return; + } + } + tries++; + m_session = std::make_shared(); + m_session->wants_online = m_online; + m_session->demo = m_demo; + m_accountToUse->fillSession(m_session); + + // Launch immediately in true offline mode + if (m_accountToUse->accountType() == AccountType::Offline) { + launchInstance(); + return; + } + + switch (m_accountToUse->accountState()) { + case AccountState::Offline: { + m_session->wants_online = false; + } + /* fallthrough */ + case AccountState::Online: { + if (!m_session->wants_online) { + // we ask the user for a player name + bool ok = false; + + QString message = tr("Choose your offline mode player name."); + if (m_session->demo) { + message = tr("Choose your demo mode player name."); + } + + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + QString usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; + QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), message, QLineEdit::Normal, usedname, &ok); + if (!ok) { + tryagain = false; + break; + } + if (name.length()) { + usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); + } + m_session->MakeOffline(usedname); + // offline flavored game from here :3 + } - if (m_accountToUse->ownsMinecraft()) { - if (!m_accountToUse->hasProfile()) { - // Now handle setting up a profile name here... - ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); - if (dialog.exec() == QDialog::Accepted) { - tryagain = true; - continue; - } else { - emitFailed(tr("Received undetermined session status during login.")); - return; - } - } - // we own Minecraft, there is a profile, it's all ready to go! - launchInstance(); - return; - } else { - // play demo ? - QMessageBox box(m_parentWidget); - box.setWindowTitle(tr("Play demo?")); - box.setText( - tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " - "the demo?")); - box.setIcon(QMessageBox::Warning); - auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); - auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); - box.setDefaultButton(cancelButton); - - box.exec(); - if (box.clickedButton() == demoButton) { - // play demo here - m_session->MakeDemo(m_session->player_name, m_session->uuid); - launchInstance(); - } else { - emitFailed(tr("Launch cancelled - account does not own Minecraft.")); - } - } + + if (m_accountToUse->ownsMinecraft()) { + + if (!m_accountToUse->hasProfile()) { + + 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_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: @@ -242,28 +303,26 @@ void LaunchController::login() case AccountState::Working: { // refresh is in progress, we need to wait for it to finish to proceed. ProgressDialog progDialog(m_parentWidget); - if (m_online) { - progDialog.setSkipButton(true, tr("Play Offline")); - } + progDialog.setSkipButton(true, tr("Abort")); + auto task = m_accountToUse->currentTask(); progDialog.execWithTask(task.get()); + + // don't retry if aborted + if (task->getState() == Task::State::AbortedByUser) + tryagain = false; + continue; } - // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that - /* - case AccountState::Queued: { - return; - } - */ case AccountState::Expired: { - auto errorString = tr("The account has expired and needs to be logged into manually again."); - QMessageBox::warning(m_parentWidget, tr("Account refresh failed"), errorString, QMessageBox::StandardButton::Ok, - QMessageBox::StandardButton::Ok); - emitFailed(errorString); + if (reauthenticateAccount(m_accountToUse)) + continue; return; } case AccountState::Disabled: { - auto errorString = tr("The launcher's client identification has changed. Please remove this account and add it again."); + auto errorString = tr("The launcher's client identification has changed. Please remove '%1' and try again.") + .arg(m_accountToUse->profileName()); + QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); @@ -271,8 +330,9 @@ void LaunchController::login() } case AccountState::Gone: { auto errorString = - tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account " - "you migrated this one to."); + tr("'%1' no longer exists on the servers. It may have been migrated, in which case please add the new account " + "you migrated this one to.") + .arg(m_accountToUse->profileName()); QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); @@ -283,6 +343,38 @@ void LaunchController::login() emitFailed(tr("Failed to launch.")); } +bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) +{ + auto button = QMessageBox::warning( + m_parentWidget, tr("Account refresh failed"), + tr("'%1' has expired and needs to be reauthenticated. Do you want to reauthenticate this account?").arg(account->profileName()), + QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); + if (button == QMessageBox::StandardButton::Yes) { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == account; + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); + if (account->accountType() == AccountType::MSA) { + auto newAccount = MSALoginDialog::newAccount(m_parentWidget); + + if (newAccount != nullptr) { + accounts->addAccount(newAccount); + + if (isDefault) + accounts->setDefaultAccount(newAccount); + + if (m_accountToUse == account) { + m_accountToUse = nullptr; + decideAccount(); + } + return true; + } + } + } + + emitFailed(tr("The account has expired and needs to be reauthenticated")); + return false; +} + void LaunchController::launchInstance() { Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); @@ -305,10 +397,10 @@ void LaunchController::launchInstance() 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; @@ -318,19 +410,18 @@ void LaunchController::launchInstance() // Prepend Server Status 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"; } - 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\n", MessageLevel::Launcher)); } m_launcher->start(); } diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index af52ad450..6580fbf0d 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -50,12 +50,14 @@ class LaunchController : public Task { LaunchController(); virtual ~LaunchController() = default; - void setInstance(InstancePtr instance) { m_instance = instance; } + void setInstance(BaseInstance* instance) { m_instance = instance; } - InstancePtr instance() { return m_instance; } + BaseInstance* instance() { return m_instance; } void setOnline(bool online) { m_online = online; } + void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } + void setDemo(bool demo) { m_demo = demo; } void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } @@ -74,8 +76,7 @@ class LaunchController : public Task { void login(); void launchInstance(); void decideAccount(); - bool askPlayDemo(); - QString askOfflineName(QString playerName, bool demo, bool& ok); + bool reauthenticateAccount(MinecraftAccountPtr account); private slots: void readyForLaunch(); @@ -87,12 +88,13 @@ class LaunchController : public Task { private: BaseProfilerFactory* m_profiler = nullptr; bool m_online = true; + QString m_offlineName; bool m_demo = false; - InstancePtr m_instance; + BaseInstance* m_instance; QWidget* m_parentWidget = nullptr; InstanceWindow* m_console = nullptr; MinecraftAccountPtr m_accountToUse = nullptr; AuthSessionPtr m_session; - shared_qobject_ptr m_launcher; + LaunchTask* m_launcher; MinecraftTarget::Ptr m_targetToJoin; }; diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 706d7022b..0e84bdd9d 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -18,89 +18,19 @@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" -# Set up env. -# Pass our custom variables separately so that the launcher can remove them for child processes -export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@" -export LAUNCHER_LD_PRELOAD="" -export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" -export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts" +# Makes the launcher use portals for file picking +export QT_QPA_PLATFORMTHEME=xdgdesktopportal -export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" -export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD" -export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH" -export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH" +# Just to be sure... +chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -# Detect missing dependencies... -DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'` -if [ "x$DEPS_LIST" = "x" ]; then - # We have all our dependencies. Run the launcher. - echo "No missing dependencies found." +ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") - # Just to be sure... - chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" - - ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") - - if [ -f portable.txt ]; then - ARGS+=("-d" "${LAUNCHER_DIR}") - fi - - ARGS+=("$@") - - # Run the launcher - exec -a "${ARGS[@]}" - - # Run the launcher in valgrind - # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" - - # Run the launcher with callgrind, delay instrumentation - # valgrind --log-file="valgrind.log" --tool=callgrind --instr-atstart=no "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" - # use callgrind_control -i on/off to profile actions - - # Exit with launcher's exit code. - # exit $? -else - # apt - if which apt-file &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" - # pacman - elif which pkgfile &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" - # dnf - elif which dnf &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do dnf whatprovides -q $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | grep -v 'Repo' | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo dnf install $COMMAND_LIBS" - # yum - elif which yum &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo yum install $COMMAND_LIBS" - # zypper - elif which zypper &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo zypper install $COMMAND_LIBS" - # emerge - elif which pfl &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo emerge $COMMAND_LIBS" - fi +if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") +fi - MESSAGE="Error: The launcher is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." - MESSAGE="$MESSAGE\n\nHint (please apply common sense): $INSTALL_CMD\n" +ARGS+=("$@") - printerror "$MESSAGE" - exit 1 -fi +# Run the launcher +exec -a "${ARGS[@]}" diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index 35ce4e0e5..bae45ad88 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -36,16 +36,16 @@ #include "LoggedProcess.h" #include -#include +#include #include "MessageLevel.h" -LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) +LoggedProcess::LoggedProcess(const QStringConverter::Encoding output_codec, QObject* parent) : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); - connect(this, QOverload::of(&QProcess::finished), this, &LoggedProcess::on_exit); + connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } @@ -57,9 +57,9 @@ LoggedProcess::~LoggedProcess() } } -QStringList LoggedProcess::reprocess(const QByteArray& data, QTextDecoder& decoder) +QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder) { - auto str = decoder.toUnicode(data); + QString str = decoder(data); if (!m_leftover_line.isEmpty()) { str.prepend(m_leftover_line); diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index 75ba15dfd..ce35b27e8 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -36,7 +36,7 @@ #pragma once #include -#include +#include #include "MessageLevel.h" /* @@ -49,7 +49,7 @@ class LoggedProcess : public QProcess { enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: - explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0); + explicit LoggedProcess(QStringConverter::Encoding outputEncoding = QStringConverter::System, QObject* parent = nullptr); virtual ~LoggedProcess(); State state() const; @@ -58,7 +58,7 @@ class LoggedProcess : public QProcess { void setDetachable(bool detachable); signals: - void log(QStringList lines, MessageLevel::Enum level); + void log(QStringList lines, MessageLevel level); void stateChanged(LoggedProcess::State state); public slots: @@ -77,11 +77,11 @@ class LoggedProcess : public QProcess { private: void changeState(LoggedProcess::State state); - QStringList reprocess(const QByteArray& data, QTextDecoder& decoder); + QStringList reprocess(const QByteArray& data, QStringDecoder& decoder); private: - QTextDecoder m_err_decoder; - QTextDecoder m_out_decoder; + QStringDecoder m_err_decoder; + QStringDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 1765fd844..252e6ac57 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -16,7 +16,6 @@ */ #include -#include #include #include @@ -99,4 +98,4 @@ QString Time::humanReadableDuration(double duration, int precision) os.flush(); return outStr; -} \ No newline at end of file +} diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index b38aca17a..8e4e433ed 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -35,66 +35,46 @@ */ #include "MMCZip.h" -#include -#include -#include +#include #include "FileSystem.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" #include #include #include #include - -#if defined(LAUNCHER_APPLICATION) -#include -#endif +#include namespace MMCZip { // ours -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter) +using FilterFunction = std::function; +#if defined(LAUNCHER_APPLICATION) +bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr) { - QuaZip modZip(from.filePath()); - modZip.open(QuaZip::mdUnzip); - - QuaZipFile fileInsideMod(&modZip); - QuaZipFile zipOutFile(into); - for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) { - QString filename = modZip.getCurrentFileName(); + ArchiveReader r(from.absoluteFilePath()); + return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { + auto filename = f->filename(); if (filter && !filter(filename)) { qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered"; - continue; + f->skip(); + return true; } if (contained.contains(filename)) { qDebug() << "Skipping already contained file " << filename << " from " << from.fileName(); - continue; + f->skip(); + return true; } contained.insert(filename); - - if (!fileInsideMod.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << filename << " from " << from.fileName(); - return false; - } - - QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); - - if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) { - qCritical() << "Failed to open " << filename << " in the jar"; - fileInsideMod.close(); - return false; - } - if (!JlCompress::copyData(fileInsideMod, zipOutFile)) { - zipOutFile.close(); - fileInsideMod.close(); + if (!into.addFile(f)) { qCritical() << "Failed to copy data of " << filename << " into the jar"; return false; } - zipOutFile.close(); - fileInsideMod.close(); - } - return true; + return true; + }); } -bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks) +bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) { QDir directory(dir); if (!directory.exists()) @@ -103,48 +83,18 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto srcPath = e.absoluteFilePath(); - if (followSymlinks) { - if (e.isSymLink()) { - srcPath = e.symLinkTarget(); - } else { - srcPath = e.canonicalFilePath(); - } - } - if (!JlCompress::compressFile(zip, srcPath, filePath)) + if (!zip.addFile(srcPath, filePath)) return false; } return true; } -bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks) -{ - QuaZip zip(fileCompressed); - zip.setUtf8Enabled(true); - QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); - if (!zip.open(QuaZip::mdCreate)) { - FS::deletePath(fileCompressed); - return false; - } - - auto result = compressDirFiles(&zip, dir, files, followSymlinks); - - zip.close(); - if (zip.getZipError() != 0) { - FS::deletePath(fileCompressed); - return false; - } - - return result; -} - -#if defined(LAUNCHER_APPLICATION) // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { - QuaZip zipOut(targetJarPath); - zipOut.setUtf8Enabled(true); - if (!zipOut.open(QuaZip::mdCreate)) { + ArchiveWriter zipOut(targetJarPath); + if (!zipOut.open()) { FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; @@ -161,7 +111,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListenabled()) continue; if (mod->type() == ResourceType::ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { + if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -170,7 +120,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListtype() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); - if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { + if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; @@ -193,7 +143,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QListfileinfo().fileName() << "to the jar."; @@ -209,7 +159,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList extractSubDir(QuaZip* zip, const QString& subdir, const QString& target) +std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - auto numEntries = zip->getEntriesCount(); - if (numEntries < 0) { + if (!zip->collectFiles()) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; - } else if (numEntries == 0) { + } + if (zip->getFiles().isEmpty()) { qDebug() << "Extracting empty archives seems odd..."; return extracted; - } else if (!zip->goToFirstFile()) { - qWarning() << "Failed to seek to first file in zip"; - return std::nullopt; } - do { - QString file_name = zip->getCurrentFileName(); - file_name = FS::RemoveInvalidPathChars(file_name); - if (!file_name.startsWith(subdir)) - continue; - - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); - auto original_name = relative_file_name; + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); + if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { + QString file_name = f->filename(); + file_name = FS::RemoveInvalidPathChars(file_name); + if (!file_name.startsWith(subdir)) { + f->skip(); + return true; + } - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); + auto original_name = relative_file_name; - relative_file_name = relative_file_name.split('/').last(); - } - - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" - << target; - return std::nullopt; - } + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - if (!JlCompress::extractFile(zip, "", target_file_path)) { - qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; - JlCompress::removeFile(extracted); - return std::nullopt; - } + relative_file_name = relative_file_name.split('/').last(); + } + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } - extracted.append(target_file_path); - auto fileInfo = QFileInfo(target_file_path); - if (fileInfo.isFile()) { - auto permissions = fileInfo.permissions(); - auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | - QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; - auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; - - auto newPermisions = (permissions & maxPermisions) | minPermisions; - if (newPermisions != permissions) { - if (!QFile::setPermissions(target_file_path, newPermisions)) { - qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); - } + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" + << target; + return false; } - } else if (fileInfo.isDir()) { - // Ensure the folder has the minimal required permissions - QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | - QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; - - QFile::Permissions currentPermissions = fileInfo.permissions(); - if ((currentPermissions & minimalPermissions) != minimalPermissions) { - if (!QFile::setPermissions(target_file_path, minimalPermissions)) { - qWarning() << (QObject::tr("Could not fix permissions for %1").arg(target_file_path)); - } + if (!f->writeFile(ext, target_file_path)) { + qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; + return false; } - } - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - } while (zip->goToNextFile()); - return extracted; -} + extracted.append(target_file_path); -// ours -bool extractRelFile(QuaZip* zip, const QString& file, const QString& target) -{ - return JlCompress::extractFile(zip, file, target); + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + return true; + })) { + qWarning() << "Failed to parse file" << zip->getZipName(); + FS::removeFiles(extracted); + return std::nullopt; + } + + return extracted; } // ours std::optional extractDir(QString fileCompressed, QString dir) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return QStringList(); - } - qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); - ; - return std::nullopt; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); } + ArchiveReader zip(fileCompressed); return extractSubDir(&zip, "", dir); } // ours std::optional extractDir(QString fileCompressed, QString subdir, QString dir) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return QStringList(); - } - qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); - ; - return std::nullopt; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return QStringList(); } + ArchiveReader zip(fileCompressed); return extractSubDir(&zip, subdir, dir); } // ours bool extractFile(QString fileCompressed, QString file, QString target) { - QuaZip zip(fileCompressed); - if (!zip.open(QuaZip::mdUnzip)) { - // check if this is a minimum size empty zip file... - QFileInfo fileInfo(fileCompressed); - if (fileInfo.size() == 22) { - return true; - } - qWarning() << "Could not open archive for unpacking:" << fileCompressed << "Error:" << zip.getZipError(); + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if (fileInfo.size() == 22) { + return true; + } + ArchiveReader zip(fileCompressed); + auto f = zip.goToFile(file); + if (!f) { return false; } - return extractRelFile(&zip, file, target); + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + return f->writeFile(ext, target); } -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) @@ -443,8 +319,8 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { - QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); - if (excludeFilter && excludeFilter(relativeFilePath)) { + if (excludeFilter && excludeFilter(e)) { + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); qDebug() << "Skipping file " << relativeFilePath; continue; } @@ -453,218 +329,4 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q } return true; } - -#if defined(LAUNCHER_APPLICATION) -void ExportToZipTask::executeTask() -{ - setStatus("Adding files..."); - setProgress(0, m_files.length()); - m_build_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); - connect(&m_build_zip_watcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); - m_build_zip_watcher.setFuture(m_build_zip_future); -} - -auto ExportToZipTask::exportZip() -> ZipResult -{ - if (!m_dir.exists()) { - return ZipResult(tr("Folder doesn't exist")); - } - if (!m_output.isOpen() && !m_output.open(QuaZip::mdCreate)) { - return ZipResult(tr("Could not create file")); - } - - for (auto fileName : m_extra_files.keys()) { - if (m_build_zip_future.isCanceled()) - return ZipResult(); - QuaZipFile indexFile(&m_output); - if (!indexFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileName))) { - return ZipResult(tr("Could not create:") + fileName); - } - indexFile.write(m_extra_files[fileName]); - } - - for (const QFileInfo& file : m_files) { - if (m_build_zip_future.isCanceled()) - return ZipResult(); - - auto absolute = file.absoluteFilePath(); - auto relative = m_dir.relativeFilePath(absolute); - setStatus("Compressing: " + relative); - setProgress(m_progress + 1, m_progressTotal); - if (m_follow_symlinks) { - if (file.isSymLink()) - absolute = file.symLinkTarget(); - else - absolute = file.canonicalFilePath(); - } - - if (!m_exclude_files.contains(relative) && !JlCompress::compressFile(&m_output, absolute, m_destination_prefix + relative)) { - return ZipResult(tr("Could not read and compress %1").arg(relative)); - } - } - - m_output.close(); - if (m_output.getZipError() != 0) { - return ZipResult(tr("A zip error occurred")); - } - return ZipResult(); -} - -void ExportToZipTask::finish() -{ - if (m_build_zip_future.isCanceled()) { - FS::deletePath(m_output_path); - emitAborted(); - } else if (auto result = m_build_zip_future.result(); result.has_value()) { - FS::deletePath(m_output_path); - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExportToZipTask::abort() -{ - if (m_build_zip_future.isRunning()) { - m_build_zip_future.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} - -void ExtractZipTask::executeTask() -{ - if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied zip file.")); - return; - } - m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); - connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); - m_zip_watcher.setFuture(m_zip_future); -} - -auto ExtractZipTask::extractZip() -> ZipResult -{ - auto target = m_output_dir.absolutePath(); - auto target_top_dir = QUrl::fromLocalFile(target); - - QStringList extracted; - - qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input->getZipName() << "to" << target; - auto numEntries = m_input->getEntriesCount(); - if (numEntries < 0) { - return ZipResult(tr("Failed to enumerate files in archive")); - } - if (numEntries == 0) { - logWarning(tr("Extracting empty archives seems odd...")); - return ZipResult(); - } - if (!m_input->goToFirstFile()) { - return ZipResult(tr("Failed to seek to first file in zip")); - } - - setStatus("Extracting files..."); - setProgress(0, numEntries); - do { - if (m_zip_future.isCanceled()) - return ZipResult(); - setProgress(m_progress + 1, m_progressTotal); - QString file_name = m_input->getCurrentFileName(); - if (!file_name.startsWith(m_subdirectory)) - continue; - - auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); - auto original_name = relative_file_name; - setStatus("Unpacking: " + relative_file_name); - - // Fix subdirs/files ending with a / getting transformed into absolute paths - if (relative_file_name.startsWith('/')) - relative_file_name = relative_file_name.mid(1); - - // Fix weird "folders with a single file get squashed" thing - QString sub_path; - if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { - sub_path = relative_file_name.section('/', 0, -2) + '/'; - FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); - - relative_file_name = relative_file_name.split('/').last(); - } - - QString target_file_path; - if (relative_file_name.isEmpty()) { - target_file_path = target + '/'; - } else { - target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); - if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) - target_file_path += '/'; - } - - if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { - return ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") - .arg(relative_file_name, target)); - } - - if (!JlCompress::extractFile(m_input.get(), "", target_file_path)) { - JlCompress::removeFile(extracted); - return ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); - } - - extracted.append(target_file_path); - auto fileInfo = QFileInfo(target_file_path); - if (fileInfo.isFile()) { - auto permissions = fileInfo.permissions(); - auto maxPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser | - QFileDevice::Permission::ReadGroup | QFileDevice::Permission::ReadOther; - auto minPermisions = QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; - - auto newPermisions = (permissions & maxPermisions) | minPermisions; - if (newPermisions != permissions) { - if (!QFile::setPermissions(target_file_path, newPermisions)) { - logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); - } - } - } else if (fileInfo.isDir()) { - // Ensure the folder has the minimal required permissions - QFile::Permissions minimalPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner | QFile::ReadGroup | - QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther; - - QFile::Permissions currentPermissions = fileInfo.permissions(); - if ((currentPermissions & minimalPermissions) != minimalPermissions) { - if (!QFile::setPermissions(target_file_path, minimalPermissions)) { - logWarning(tr("Could not fix permissions for %1").arg(target_file_path)); - } - } - } - - qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; - } while (m_input->goToNextFile()); - - return ZipResult(); -} - -void ExtractZipTask::finish() -{ - if (m_zip_future.isCanceled()) { - emitAborted(); - } else if (auto result = m_zip_future.result(); result.has_value()) { - emitFailed(result.value()); - } else { - emitSucceeded(); - } -} - -bool ExtractZipTask::abort() -{ - if (m_zip_future.isRunning()) { - m_zip_future.cancel(); - // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur - // immediately. - return true; - } - return false; -} - -#endif } // namespace MMCZip diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index d81df9d81..04fe90379 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -36,8 +36,6 @@ #pragma once -#include -#include #include #include #include @@ -46,41 +44,15 @@ #include #include #include -#include #include +#include "archive/ArchiveReader.h" #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif -#include "tasks/Task.h" namespace MMCZip { -using FilterFunction = std::function; - -/** - * Merge two zip files, using a filter function - */ -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr); - -/** - * Compress directory, by providing a list of files to compress - * \param zip target archive - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ -bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool followSymlinks = false); - -/** - * Compress directory, by providing a list of files to compress - * \param fileCompressed target archive file - * \param dir directory that will be compressed (to compress with relative paths) - * \param files list of files to compress - * \param followSymlinks should follow symlinks when compressing file data - * \return true for success or false for failure - */ -bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); +using FilterFileFunction = std::function; #if defined(LAUNCHER_APPLICATION) /** @@ -88,29 +60,11 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); #endif -/** - * Find a single file in archive by file name (not path) - * - * \param ignore_paths paths to skip when recursing the search - * - * \return the path prefix where the file is - */ -QString findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths = {}, const QString& root = QString("")); - -/** - * Find a multiple files of the same name in archive by file name - * If a file is found in a path, no deeper paths are searched - * - * \return true if anything was found - */ -bool findFilesInZip(QuaZip* zip, const QString& what, QStringList& result, const QString& root = QString()); /** * Extract a subdirectory from an archive */ -std::optional extractSubDir(QuaZip* zip, const QString& subdir, const QString& target); - -bool extractRelFile(QuaZip* zip, const QString& file, const QString& target); +std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target); /** * Extract a whole archive. @@ -149,91 +103,5 @@ bool extractFile(QString fileCompressed, QString file, QString dir); * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); - -#if defined(LAUNCHER_APPLICATION) -class ExportToZipTask : public Task { - Q_OBJECT - public: - ExportToZipTask(QString outputPath, - QDir dir, - QFileInfoList files, - QString destinationPrefix = "", - bool followSymlinks = false, - bool utf8Enabled = false) - : m_output_path(outputPath) - , m_output(outputPath) - , m_dir(dir) - , m_files(files) - , m_destination_prefix(destinationPrefix) - , m_follow_symlinks(followSymlinks) - { - setAbortable(true); - m_output.setUtf8Enabled(utf8Enabled); - }; - ExportToZipTask(QString outputPath, - QString dir, - QFileInfoList files, - QString destinationPrefix = "", - bool followSymlinks = false, - bool utf8Enabled = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; - - virtual ~ExportToZipTask() = default; - - void setExcludeFiles(QStringList excludeFiles) { m_exclude_files = excludeFiles; } - void addExtraFile(QString fileName, QByteArray data) { m_extra_files.insert(fileName, data); } - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult exportZip(); - void finish(); - - private: - QString m_output_path; - QuaZip m_output; - QDir m_dir; - QFileInfoList m_files; - QString m_destination_prefix; - bool m_follow_symlinks; - QStringList m_exclude_files; - QHash m_extra_files; - - QFuture m_build_zip_future; - QFutureWatcher m_build_zip_watcher; -}; - -class ExtractZipTask : public Task { - Q_OBJECT - public: - ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") - : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) - {} - ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") - : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) - {} - virtual ~ExtractZipTask() = default; - - using ZipResult = std::optional; - - protected: - virtual void executeTask() override; - bool abort() override; - - ZipResult extractZip(); - void finish(); - - private: - std::shared_ptr m_input; - QDir m_output_dir; - QString m_subdirectory; - - QFuture m_zip_future; - QFutureWatcher m_zip_watcher; -}; -#endif +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); } // namespace MMCZip diff --git a/launcher/MangoHud.cpp b/launcher/MangoHud.cpp index 29a7c63d9..d85100207 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/MangoHud.cpp @@ -111,8 +111,8 @@ QString getLibraryString() try { auto conf = Json::requireDocument(filePath, vkLayer); auto confObject = Json::requireObject(conf, vkLayer); - auto layer = Json::ensureObject(confObject, "layer"); - QString libraryName = Json::ensureString(layer, "library_path"); + auto layer = confObject["layer"].toObject(); + QString libraryName = layer["library_path"].toString(); if (libraryName.isEmpty()) { continue; diff --git a/launcher/Markdown.cpp b/launcher/Markdown.cpp index 426067bf6..6f6d34828 100644 --- a/launcher/Markdown.cpp +++ b/launcher/Markdown.cpp @@ -28,4 +28,4 @@ QString markdownToHTML(const QString& markdown) free(buffer); return htmlStr; -} \ No newline at end of file +} diff --git a/launcher/Markdown.h b/launcher/Markdown.h index f91a016bd..57a2e5437 100644 --- a/launcher/Markdown.h +++ b/launcher/Markdown.h @@ -21,4 +21,4 @@ #include #include -QString markdownToHTML(const QString& markdown); \ No newline at end of file +QString markdownToHTML(const QString& markdown); diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 116e70c4b..e5f4eb19a 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -1,20 +1,23 @@ #include "MessageLevel.h" -MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) +MessageLevel MessageLevel::fromName(const QString& levelName) { - if (levelName == "Launcher") + QString name = levelName.toUpper(); + if (name == "LAUNCHER") return MessageLevel::Launcher; - else if (levelName == "Debug") + else if (name == "TRACE") + return MessageLevel::Trace; + else if (name == "DEBUG") return MessageLevel::Debug; - else if (levelName == "Info") + else if (name == "INFO") return MessageLevel::Info; - else if (levelName == "Message") + else if (name == "MESSAGE") return MessageLevel::Message; - else if (levelName == "Warning") + else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; - else if (levelName == "Error") + else if (name == "ERROR" || name == "CRITICAL") return MessageLevel::Error; - else if (levelName == "Fatal") + else if (name == "FATAL") return MessageLevel::Fatal; // Skip PrePost, it's not exposed to !![]! // Also skip StdErr and StdOut @@ -22,12 +25,47 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) return MessageLevel::Unknown; } -MessageLevel::Enum MessageLevel::fromLine(QString& line) +MessageLevel MessageLevel::fromQtMsgType(const QtMsgType& type) +{ + switch (type) { + case QtDebugMsg: + return MessageLevel::Debug; + case QtInfoMsg: + return MessageLevel::Info; + case QtWarningMsg: + return MessageLevel::Warning; + case QtCriticalMsg: + return MessageLevel::Error; + case QtFatalMsg: + return MessageLevel::Fatal; + default: + return MessageLevel::Unknown; + } +} + +/* Get message level from a line. Line is modified if it was successful. */ +MessageLevel MessageLevel::takeFromLine(QString& line) { // Level prefix int endmark = line.indexOf("]!"); if (line.startsWith("!![") && endmark != -1) { - auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); + auto level = MessageLevel::fromName(line.left(endmark).mid(3)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} + +/* Get message level from a line from the launcher log. Line is modified if it was successful. */ +MessageLevel MessageLevel::takeFromLauncherLine(QString& line) +{ + // Level prefix + int startMark = 0; + while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) + ++startMark; + int endmark = line.indexOf(":"); + if (startMark < line.size() && endmark != -1) { + auto level = MessageLevel::fromName(line.left(endmark).mid(startMark)); line = line.mid(endmark + 2); return level; } diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index fd12583f2..cff098664 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,26 +1,43 @@ #pragma once #include +#include /** * @brief the MessageLevel Enum * defines what level a log message is */ -namespace MessageLevel { -enum Enum { - Unknown, /**< No idea what this is or where it came from */ - StdOut, /**< Undetermined stderr messages */ - StdErr, /**< Undetermined stdout messages */ - Launcher, /**< Launcher Messages */ - Debug, /**< Debug Messages */ - Info, /**< Info Messages */ - Message, /**< Standard Messages */ - Warning, /**< Warnings */ - Error, /**< Errors */ - Fatal, /**< Fatal Errors */ -}; -MessageLevel::Enum getLevel(const QString& levelName); +struct MessageLevel { + enum class Enum { + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + Launcher, /**< Launcher Messages */ + Trace, /**< Trace Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ + }; + using enum Enum; + constexpr MessageLevel(Enum e = Unknown) : m_type(e) {} + static MessageLevel fromName(const QString& type); + static MessageLevel fromQtMsgType(const QtMsgType& type); + static MessageLevel fromLine(const QString& line); + inline bool isValid() const { return m_type != Unknown; } + std::strong_ordering operator<=>(const MessageLevel& other) const = default; + std::strong_ordering operator<=>(const MessageLevel::Enum& other) const { return m_type <=> other; } + explicit operator int() const { return static_cast(m_type); } + explicit operator MessageLevel::Enum() { return m_type; } + + /* Get message level from a line. Line is modified if it was successful. */ + static MessageLevel takeFromLine(QString& line); -/* Get message level from a line. Line is modified if it was successful. */ -MessageLevel::Enum fromLine(QString& line); -} // namespace MessageLevel + /* Get message level from a line from the launcher log. Line is modified if it was successful. */ + static MessageLevel takeFromLauncherLine(QString& line); + + private: + Enum m_type; +}; diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 3d01c9d33..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,13 +52,12 @@ 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(); } QMap getVariables() override { return QMap(); } - IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } - QString getLogFileRoot() override { return instanceRoot(); } + QStringList getLogFileSearchPaths() override { return {}; } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } @@ -71,7 +70,7 @@ class NullInstance : public BaseInstance { return out; } QString modsRoot() const override { return QString(); } - void updateRuntimeContext() + void updateRuntimeContext() override { // NOOP } diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h index ba6154ad8..67a57a1a2 100644 --- a/launcher/PSaveFile.h +++ b/launcher/PSaveFile.h @@ -68,4 +68,4 @@ class PSaveFile : public QSaveFile { }; #else #define PSaveFile QSaveFile -#endif \ No newline at end of file +#endif diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h index a1c64b433..88c17c0b2 100644 --- a/launcher/QObjectPtr.h +++ b/launcher/QObjectPtr.h @@ -33,7 +33,7 @@ class shared_qobject_ptr : public QSharedPointer { {} void reset() { QSharedPointer::reset(); } - void reset(T*&& other) + void reset(T* other) { shared_qobject_ptr t(other); this->swap(t); diff --git a/launcher/QVariantUtils.h b/launcher/QVariantUtils.h index 91f2ad29c..23fe82573 100644 --- a/launcher/QVariantUtils.h +++ b/launcher/QVariantUtils.h @@ -66,4 +66,4 @@ inline QVariant fromList(QList val) return variantList; } -} // namespace QVariantUtils \ No newline at end of file +} // namespace QVariantUtils diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index 8b28a03f1..b0137fb5c 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -1,7 +1,6 @@ #include "RecursiveFileSystemWatcher.h" #include -#include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { @@ -79,7 +78,7 @@ QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) } for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) { auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); - if (m_matcher->matches(relPath)) { + if (m_matcher(relPath)) { ret.append(relPath); } } diff --git a/launcher/RecursiveFileSystemWatcher.h b/launcher/RecursiveFileSystemWatcher.h index 7f96f5cd0..0a71e64c2 100644 --- a/launcher/RecursiveFileSystemWatcher.h +++ b/launcher/RecursiveFileSystemWatcher.h @@ -2,7 +2,7 @@ #include #include -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" class RecursiveFileSystemWatcher : public QObject { Q_OBJECT @@ -16,7 +16,7 @@ class RecursiveFileSystemWatcher : public QObject { void setWatchFiles(bool watchFiles); bool watchFiles() const { return m_watchFiles; } - void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; } + void setMatcher(Filter matcher) { m_matcher = std::move(matcher); } QStringList files() const { return m_files; } @@ -32,7 +32,7 @@ class RecursiveFileSystemWatcher : public QObject { QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; - IPathMatcher::Ptr m_matcher; + Filter m_matcher; QFileSystemWatcher* m_watcher; diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 6f5b9a189..01c4cdc18 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -21,23 +21,23 @@ #include "Application.h" -#include "minecraft/mod/ModFolderModel.h" +#include "FileSystem.h" #include "minecraft/mod/ResourceFolderModel.h" +#include "minecraft/mod/ShaderPackFolderModel.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - const std::shared_ptr packs, - bool is_indexed, - QString custom_target_folder) - : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs), m_custom_target_folder(custom_target_folder) + ResourceFolderModel* packs, + bool is_indexed) + : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) { - if (auto model = dynamic_cast(m_pack_model.get()); model && is_indexed) { - m_update_task.reset(new LocalModUpdateTask(model->indexDir(), *m_pack, m_pack_version)); - connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ResourceDownloadTask::hasOldResource); + if (is_indexed) { + m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); + connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); addTask(m_update_task); } @@ -45,17 +45,7 @@ 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)); - QDir dir{ m_pack_model->dir() }; - { - // FIXME: Make this more generic. May require adding additional info to IndexedVersion, - // or adquiring a reference to the base instance. - if (!m_custom_target_folder.isEmpty()) { - dir.cdUp(); - dir.cd(m_custom_target_folder); - } - } - - auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename())); + auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: @@ -89,13 +79,25 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, void ResourceDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); - auto name = std::get<0>(to_delete); - auto filename = std::get<1>(to_delete); - if (!name.isEmpty() && filename != m_pack_version.fileName) { - if (auto model = dynamic_cast(m_pack_model.get()); model) - model->uninstallMod(filename, true); - else - m_pack_model->uninstallResource(filename); + auto oldName = std::get<0>(to_delete); + auto oldFilename = std::get<1>(to_delete); + + if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) + return; + + m_pack_model->uninstallResource(oldFilename, true); + + // also rename the shader config file + if (dynamic_cast(m_pack_model) != nullptr) { + QFileInfo oldConfig(m_pack_model->dir(), oldFilename + ".txt"); + QFileInfo newConfig(m_pack_model->dir(), getFilename() + ".txt"); + + if (oldConfig.exists() && !newConfig.exists()) { + bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); + + if (!success) + emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); + } } } diff --git a/launcher/ResourceDownloadTask.h b/launcher/ResourceDownloadTask.h index f686e819a..7a04c6f1c 100644 --- a/launcher/ResourceDownloadTask.h +++ b/launcher/ResourceDownloadTask.h @@ -22,7 +22,7 @@ #include "net/NetJob.h" #include "tasks/SequentialTask.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/ModIndex.h" class ResourceFolderModel; @@ -32,11 +32,9 @@ class ResourceDownloadTask : public SequentialTask { public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, - std::shared_ptr packs, - bool is_indexed = true, - QString custom_target_folder = {}); + ResourceFolderModel* packs, + bool is_indexed = true); const QString& getFilename() const { return m_pack_version.fileName; } - const QString& getCustomPath() const { return m_custom_target_folder; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } @@ -46,11 +44,10 @@ class ResourceDownloadTask : public SequentialTask { private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; - const std::shared_ptr m_pack_model; - QString m_custom_target_folder; + ResourceFolderModel* m_pack_model; NetJob::Ptr m_filesNetJob; - LocalModUpdateTask::Ptr m_update_task; + LocalResourceUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); void downloadFailed(QString reason); diff --git a/launcher/RuntimeContext.h b/launcher/RuntimeContext.h index 85304a5bc..84a56a892 100644 --- a/launcher/RuntimeContext.h +++ b/launcher/RuntimeContext.h @@ -41,7 +41,7 @@ struct RuntimeContext { return javaRealArchitecture; } - void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) + void updateFromInstanceSettings(SettingsObject* instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index edda9f247..a79cfb5c5 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -35,7 +35,6 @@ */ #include "StringUtils.h" -#include #include #include @@ -53,7 +52,7 @@ static inline QChar getNextChar(const QString& s, int location) int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; - while (l1 <= s1.count() && l2 <= s2.count()) { + while (l1 <= s1.size() && l2 <= s2.size()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) @@ -213,11 +212,10 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular return qMakePair(left, right); } -static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); - QString StringUtils::htmlListPatch(QString htmlStr) { - int pos = htmlStr.indexOf(ulMatcher); + static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); + int pos = htmlStr.indexOf(s_ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index @@ -230,7 +228,7 @@ QString StringUtils::htmlListPatch(QString htmlStr) if (textBetween.isEmpty()) htmlStr.insert(pos, "
"); - pos = htmlStr.indexOf(ulMatcher, pos); + pos = htmlStr.indexOf(s_ulMatcher, pos); } return htmlStr; -} \ No newline at end of file +} diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h index f3688d60d..f6c04d702 100644 --- a/launcher/SysInfo.h +++ b/launcher/SysInfo.h @@ -1,3 +1,4 @@ +#pragma once #include namespace SysInfo { diff --git a/launcher/Untar.cpp b/launcher/Untar.cpp deleted file mode 100644 index f1963e7aa..000000000 --- a/launcher/Untar.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023-2024 Trial97 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "Untar.h" -#include -#include -#include -#include -#include -#include "FileSystem.h" - -// adaptation of the: -// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c -// - https://en.wikipedia.org/wiki/Tar_(computing) -// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp - -#define BLOCKSIZE 512 -#define SHORTNAMESIZE 100 - -enum class TypeFlag : char { - Regular = '0', // regular file - ARegular = 0, // regular file - Link = '1', // link - Symlink = '2', // reserved - Character = '3', // character special - Block = '4', // block special - Directory = '5', // directory - FIFO = '6', // FIFO special - Contiguous = '7', // reserved - // Posix stuff - GlobalPosixHeader = 'g', - ExtendedPosixHeader = 'x', - // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) - // GNU - GNULongLink = 'K', /* long link name */ - GNULongName = 'L', /* long file name */ -}; - -// struct Header { /* byte offset */ -// char name[100]; /* 0 */ -// char mode[8]; /* 100 */ -// char uid[8]; /* 108 */ -// char gid[8]; /* 116 */ -// char size[12]; /* 124 */ -// char mtime[12]; /* 136 */ -// char chksum[8]; /* 148 */ -// TypeFlag typeflag; /* 156 */ -// char linkname[100]; /* 157 */ -// char magic[6]; /* 257 */ -// char version[2]; /* 263 */ -// char uname[32]; /* 265 */ -// char gname[32]; /* 297 */ -// char devmajor[8]; /* 329 */ -// char devminor[8]; /* 337 */ -// char prefix[155]; /* 345 */ -// /* 500 */ -// }; - -bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) -{ - qint64 n = 0; - size--; // ignore trailing null - if (size < 0) { - qCritical() << "The filename size is negative"; - return false; - } - longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE - for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) { - n = in->read(longlink.data() + offset, BLOCKSIZE); - if (n != BLOCKSIZE) { - qCritical() << "The expected blocksize was not respected for the name"; - return false; - } - } - longlink.truncate(qstrlen(longlink.constData())); - return true; -} - -int getOctal(char* buffer, int maxlenght, bool* ok) -{ - return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); -} - -QString decodeName(char* name) -{ - return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); -} -bool Tar::extract(QIODevice* in, QString dst) -{ - char buffer[BLOCKSIZE]; - QString name, symlink, firstFolderName; - bool doNotReset = false, ok; - while (true) { - auto n = in->read(buffer, BLOCKSIZE); - if (n != BLOCKSIZE) { // allways expect complete blocks - qCritical() << "The expected blocksize was not respected"; - return false; - } - if (buffer[0] == 0) { // end of archive - return true; - } - int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions - if (!ok) { - qCritical() << "The file mode can't be read"; - return false; - } - // there are names that are exactly 100 bytes long - // and neither longlink nor \0 terminated (bug:101472) - - if (name.isEmpty()) { - name = decodeName(buffer); - if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) { - name = name.mid(firstFolderName.size()); - } - } - if (symlink.isEmpty()) - symlink = decodeName(buffer); - qint64 size = getOctal(buffer + 124, 12, &ok); - if (!ok) { - qCritical() << "The file size can't be read"; - return false; - } - switch (TypeFlag(buffer[156])) { - case TypeFlag::Regular: - /* fallthrough */ - case TypeFlag::ARegular: { - auto fileName = FS::PathCombine(dst, name); - if (!FS::ensureFilePathExists(fileName)) { - qCritical() << "Can't ensure the file path to exist: " << fileName; - return false; - } - QFile out(fileName); - if (!out.open(QFile::WriteOnly)) { - qCritical() << "Can't open file:" << fileName; - return false; - } - out.setPermissions(QFile::Permissions(mode)); - while (size > 0) { - QByteArray tmp(BLOCKSIZE, 0); - n = in->read(tmp.data(), BLOCKSIZE); - if (n != BLOCKSIZE) { - qCritical() << "The expected blocksize was not respected when reading file"; - return false; - } - tmp.truncate(qMin(qint64(BLOCKSIZE), size)); - out.write(tmp); - size -= BLOCKSIZE; - } - break; - } - case TypeFlag::Directory: { - if (firstFolderName.isEmpty()) { - firstFolderName = name; - break; - } - auto folderPath = FS::PathCombine(dst, name); - if (!FS::ensureFolderPathExists(folderPath)) { - qCritical() << "Can't ensure that folder exists: " << folderPath; - return false; - } - break; - } - case TypeFlag::GNULongLink: { - doNotReset = true; - QByteArray longlink; - if (readLonglink(in, size, longlink)) { - symlink = QFile::decodeName(longlink.constData()); - } else { - qCritical() << "Failed to read long link"; - return false; - } - break; - } - case TypeFlag::GNULongName: { - doNotReset = true; - QByteArray longlink; - if (readLonglink(in, size, longlink)) { - name = QFile::decodeName(longlink.constData()); - } else { - qCritical() << "Failed to read long name"; - return false; - } - break; - } - case TypeFlag::Link: - /* fallthrough */ - case TypeFlag::Symlink: { - auto fileName = FS::PathCombine(dst, name); - if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks - qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); - return false; - } - FS::ensureFilePathExists(fileName); - QFile::setPermissions(fileName, QFile::Permissions(mode)); - break; - } - case TypeFlag::Character: - /* fallthrough */ - case TypeFlag::Block: - /* fallthrough */ - case TypeFlag::FIFO: - /* fallthrough */ - case TypeFlag::Contiguous: - /* fallthrough */ - case TypeFlag::GlobalPosixHeader: - /* fallthrough */ - case TypeFlag::ExtendedPosixHeader: - /* fallthrough */ - default: - break; - } - if (!doNotReset) { - name.truncate(0); - symlink.truncate(0); - } - doNotReset = false; - } - return true; -} - -bool GZTar::extract(QString src, QString dst) -{ - QuaGzipFile a(src); - if (!a.open(QIODevice::ReadOnly)) { - qCritical() << "Can't open tar file:" << src; - return false; - } - return Tar::extract(&a, dst); -} \ No newline at end of file diff --git a/launcher/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 2edb17e72..bffe5d58a 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,7 +1,6 @@ #include "Version.h" #include -#include #include #include @@ -79,7 +78,7 @@ void Version::parse() if (m_string.isEmpty()) return; - auto classChange = [&](QChar lastChar, QChar currentChar) { + auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { if (lastChar.isNull()) return false; if (lastChar.isDigit() != currentChar.isDigit()) diff --git a/launcher/Version.h b/launcher/Version.h index b06e256aa..4b5ea7119 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -72,22 +72,14 @@ class Version { } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto numPart = QStringView{ m_fullString }.left(cutoff); -#else - auto numPart = m_fullString.leftRef(cutoff); -#endif if (!numPart.isEmpty()) { m_isNull = false; m_numPart = numPart.toInt(); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto stringPart = QStringView{ m_fullString }.mid(cutoff); -#else - auto stringPart = m_fullString.midRef(cutoff); -#endif if (!stringPart.isEmpty()) { m_isNull = false; @@ -104,8 +96,8 @@ class Version { QString m_fullString; - [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } - [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } + inline bool 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 { diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 7538ce08c..32048db8e 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -37,9 +37,9 @@ #include "VersionProxyModel.h" #include #include +#include #include #include -#include "Application.h" class VersionFilterModel : public QSortFilterProxyModel { Q_OBJECT @@ -63,7 +63,7 @@ class VersionFilterModel : public QSortFilterProxyModel { for (auto it = filters.begin(); it != filters.end(); ++it) { auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); - if (!it.value()->accepts(match)) { + if (!it.value()(match)) { return false; } } @@ -193,8 +193,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const if (value.toBool()) { return tr("Recommended"); } else if (hasLatest) { - auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (value.toBool()) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { return tr("Latest"); } } @@ -203,33 +203,27 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const } } case Qt::DecorationRole: { - switch (column) { - case Name: { - if (hasRecommended) { - auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); - if (recommenced.toBool()) { - return APPLICATION->getThemedIcon("star"); - } else if (hasLatest) { - auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (latest.toBool()) { - return APPLICATION->getThemedIcon("bug"); - } - } - QPixmap pixmap; - QPixmapCache::find("placeholder", &pixmap); - if (!pixmap) { - QPixmap px(16, 16); - px.fill(Qt::transparent); - QPixmapCache::insert("placeholder", px); - return px; - } - return pixmap; + if (column == Name && hasRecommended) { + auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (recommenced.toBool()) { + return QIcon::fromTheme("star"); + } else if (hasLatest) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { + return QIcon::fromTheme("bug"); } } - default: { - return QVariant(); + QPixmap pixmap; + QPixmapCache::find("placeholder", &pixmap); + if (!pixmap) { + QPixmap px(16, 16); + px.fill(Qt::transparent); + QPixmapCache::insert("placeholder", px); + return px; } + return pixmap; } + return QVariant(); } default: { if (roles.contains((BaseVersionList::ModelRoles)role)) { @@ -301,13 +295,11 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); - beginResetModel(); m_columns.clear(); if (!replacing) { roles.clear(); filterModel->setSourceModel(replacing); - endResetModel(); return; } @@ -349,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) hasLatest = true; } filterModel->setSourceModel(replacing); - - endResetModel(); } QModelIndex VersionProxyModel::getRecommended() const @@ -390,9 +380,9 @@ void VersionProxyModel::clearFilters() filterModel->filterChanged(); } -void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter* f) +void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) { - m_filters[column].reset(f); + m_filters[column] = std::move(f); filterModel->filterChanged(); } diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 7965af0ad..ddd5d2458 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -10,7 +10,7 @@ class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; - using FilterMap = QHash>; + using FilterMap = QHash; public: VersionProxyModel(QObject* parent = 0); @@ -28,7 +28,7 @@ class VersionProxyModel : public QAbstractProxyModel { const FilterMap& filters() const; const QString& search() const; - void setFilter(BaseVersionList::ModelRoles column, Filter* filter); + void setFilter(BaseVersionList::ModelRoles column, Filter filter); void setSearch(const QString& search); void clearFilters(); QModelIndex getRecommended() const; diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp new file mode 100644 index 000000000..f56fd0f0a --- /dev/null +++ b/launcher/archive/ArchiveReader.cpp @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Additional note: Portions of this file are released into the public domain + * under LicenseRef-PublicDomain. + */ +#include "ArchiveReader.h" +#include +#include +#include +#include +#include + +namespace MMCZip { +QStringList ArchiveReader::getFiles() +{ + return m_fileNames; +} + +bool ArchiveReader::collectFiles(bool onlyFiles) +{ + return parse([this, onlyFiles](File* f) { + if (!onlyFiles || f->isFile()) + m_fileNames << f->filename(); + return f->skip(); + }); +} + +QString ArchiveReader::File::filename() +{ + return QString::fromUtf8(archive_entry_pathname_utf8(m_entry)); +} + +QByteArray ArchiveReader::File::readAll(int* outStatus) +{ + QByteArray data; + const void* buff; + size_t size; + la_int64_t offset; + + int status; + while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { + data.append(static_cast(buff), static_cast(size)); + } + if (status != ARCHIVE_EOF && status != ARCHIVE_OK) { + qWarning() << "libarchive read error: " << archive_error_string(m_archive.get()); + } + if (outStatus) { + *outStatus = status; + } + return data; +} + +QDateTime ArchiveReader::File::dateTime() +{ + auto mtime = archive_entry_mtime(m_entry); + auto mtime_nsec = archive_entry_mtime_nsec(m_entry); + auto dt = QDateTime::fromSecsSinceEpoch(mtime); + return dt.addMSecs(mtime_nsec / 1e6); +} + +int ArchiveReader::File::readNextHeader() +{ + return archive_read_next_header(m_archive.get(), &m_entry); +} + +auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr +{ + auto f = std::make_unique(); + auto a = f->m_archive.get(); + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + auto fileName = m_archivePath.toStdWString(); + if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a); + return nullptr; + } + + while (f->readNextHeader() == ARCHIVE_OK) { + if (f->filename() == filename) { + return f; + } + f->skip(); + } + + archive_read_close(a); + return nullptr; +} + +static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) +{ + int r; + const void* buff; + size_t size; + la_int64_t offset; + + for (;;) { + r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + return (ARCHIVE_OK); + if (r < ARCHIVE_OK) { + qCritical() << "Failed reading data block:" << archive_error_string(ar); + return (r); + } + if (notBlock) { + r = archive_write_data(aw, buff, size); + } else { + r = archive_write_data_block(aw, buff, size, offset); + } + if (r < ARCHIVE_OK) { + qCritical() << "Failed writing data block:" << archive_error_string(aw); + return (r); + } + } +} + +bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) +{ + auto entry = m_entry; + std::unique_ptr entryClone(nullptr, &archive_entry_free); + if (!targetFileName.isEmpty()) { + entryClone.reset(archive_entry_clone(m_entry)); + entry = entryClone.get(); + auto nameUtf8 = targetFileName.toUtf8(); + archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); + } + if (archive_write_header(out, entry) < ARCHIVE_OK) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); + return false; + } else if (archive_entry_size(m_entry) > 0) { + auto r = copy_data(m_archive.get(), out, notBlock); + if (r < ARCHIVE_OK) + qCritical() << "Failed reading data block:" << archive_error_string(out); + if (r < ARCHIVE_WARN) + return false; + } + auto r = archive_write_finish_entry(out); + if (r < ARCHIVE_OK) + qCritical() << "Failed to finish writing entry:" << archive_error_string(out); + return (r >= ARCHIVE_WARN); +} + +bool ArchiveReader::parse(std::function doStuff) +{ + auto f = std::make_unique(); + auto a = f->m_archive.get(); + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + auto fileName = m_archivePath.toStdWString(); + if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error(); + return false; + } + + bool breakControl = false; + while (f->readNextHeader() == ARCHIVE_OK) { + if (f && !doStuff(f.get(), breakControl)) { + qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error(); + return false; + } + if (breakControl) { + break; + } + } + + archive_read_close(a); + return true; +} + +bool ArchiveReader::parse(std::function doStuff) +{ + return parse([doStuff](File* f, bool&) { return doStuff(f); }); +} + +bool ArchiveReader::File::isFile() +{ + return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG; +} +bool ArchiveReader::File::skip() +{ + return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK; +} +const char* ArchiveReader::File::error() +{ + return archive_error_string(m_archive.get()); +} +QString ArchiveReader::getZipName() +{ + return m_archivePath; +} + +bool ArchiveReader::exists(const QString& filePath) const +{ + if (filePath == QLatin1String("/") || filePath.isEmpty()) + return true; + // Normalize input path (remove trailing slash, if any) + QString normalizedPath = QDir::cleanPath(filePath); + if (normalizedPath.startsWith('/')) + normalizedPath.remove(0, 1); + if (normalizedPath == QLatin1String(".")) + return true; + if (normalizedPath == QLatin1String("..")) + return false; // root only + + // Check for exact file match + if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) + return true; + + // Check for directory existence by seeing if any file starts with that path + QString dirPath = normalizedPath + QLatin1Char('/'); + for (const QString& f : m_fileNames) { + if (f.startsWith(dirPath, Qt::CaseInsensitive)) + return true; + } + + return false; +} + +ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {} +} // namespace MMCZip diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h new file mode 100644 index 000000000..aaeba6095 --- /dev/null +++ b/launcher/archive/ArchiveReader.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +struct archive; +struct archive_entry; +namespace MMCZip { +class ArchiveReader { + public: + using ArchivePtr = std::unique_ptr; + ArchiveReader(QString fileName) : m_archivePath(fileName) {} + virtual ~ArchiveReader() = default; + + QStringList getFiles(); + QString getZipName(); + bool collectFiles(bool onlyFiles = true); + bool exists(const QString& filePath) const; + + class File { + public: + File(); + virtual ~File() = default; + + QString filename(); + bool isFile(); + QDateTime dateTime(); + const char* error(); + + QByteArray readAll(int* outStatus = nullptr); + bool skip(); + bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); + + private: + int readNextHeader(); + + private: + friend ArchiveReader; + ArchivePtr m_archive; + archive_entry* m_entry; + }; + + std::unique_ptr goToFile(QString filename); + bool parse(std::function); + bool parse(std::function); + + private: + QString m_archivePath; + size_t m_blockSize = 10240; + + QStringList m_fileNames = {}; +}; +} // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.cpp b/launcher/archive/ArchiveWriter.cpp new file mode 100644 index 000000000..a546f2bb7 --- /dev/null +++ b/launcher/archive/ArchiveWriter.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ArchiveWriter.h" +#include +#include +#include + +#include +#include + +#include + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +// clang-format off +#include +#include +// clang-format on +#endif + +namespace MMCZip { + +ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {} + +ArchiveWriter::~ArchiveWriter() +{ + close(); +} + +bool ArchiveWriter::open() +{ + if (m_filename.isEmpty()) { + qCritical() << "Archive m_filename not set."; + return false; + } + + m_archive = archive_write_new(); + if (!m_archive) { + qCritical() << "Archive not initialized."; + return false; + } + + auto format = m_format.toUtf8(); + archive_write_set_format_by_name(m_archive, format.constData()); + + if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); + return false; + } + + auto archiveNameW = m_filename.toStdWString(); + if (archive_write_open_filename_w(m_archive, archiveNameW.data()) != ARCHIVE_OK) { + qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); + return false; + } + + return true; +} + +bool ArchiveWriter::close() +{ + bool success = true; + if (m_archive) { + if (archive_write_close(m_archive) != ARCHIVE_OK) { + qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive); + success = false; + } + if (archive_write_free(m_archive) != ARCHIVE_OK) { + qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive); + success = false; + } + m_archive = nullptr; + } + return success; +} + +bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) +{ + QFileInfo fileInfo(fileName); + if (!fileInfo.exists()) { + qCritical() << "File does not exist:" << fileInfo.filePath(); + return false; + } + + std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); + auto entry = entry_ptr.get(); + if (!entry) { + qCritical() << "Failed to create archive entry"; + return false; + } + + auto fileDestUtf8 = fileDest.toUtf8(); + archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); + +#if defined Q_OS_WIN32 + { + // Windows needs to use this method, thanks I hate it. + + auto widePath = fileInfo.absoluteFilePath().toStdWString(); + HANDLE file_handle = CreateFileW(widePath.data(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (file_handle == INVALID_HANDLE_VALUE) { + qCritical() << "Failed to stat file:" << fileInfo.filePath(); + return false; + } + + BY_HANDLE_FILE_INFORMATION file_info; + if (!GetFileInformationByHandle(file_handle, &file_info)) { + qCritical() << "Failed to stat file:" << fileInfo.filePath(); + CloseHandle(file_handle); + return false; + } + + archive_entry_copy_bhfi(entry, &file_info); + CloseHandle(file_handle); + } +#else + { + // this only works for multibyte encoded filenames if the local is properly set, + // a wide character version doesn't seem to exist: here's hoping... + + QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8(); + const char* cpath = utf8.constData(); + struct stat st; + if (stat(cpath, &st) != 0) { + qCritical() << "Failed to stat file:" << fileInfo.filePath(); + return false; + } + + // This should handle the copying of most attributes + archive_entry_copy_stat(entry, &st); + } +#endif + + // However: + // "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]." + // - `archive_entry_stat(3)` + if (fileInfo.isSymLink()) { + archive_entry_set_filetype(entry, AE_IFLNK); + + // We also need to manually copy some attributes from the link itself, as `stat` above operates on its target + auto target = fileInfo.symLinkTarget().toUtf8(); + archive_entry_set_symlink_utf8(entry, target.constData()); + archive_entry_set_size(entry, 0); + archive_entry_set_perm(entry, fileInfo.permissions()); + } else if (fileInfo.isFile()) { + archive_entry_set_filetype(entry, AE_IFREG); + } else { + qCritical() << "Unsupported file type:" << fileInfo.filePath(); + return false; + } + + if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { + qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + + if (fileInfo.isFile() && !fileInfo.isSymLink()) { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file: " << fileInfo.filePath(); + return false; + } + + constexpr qint64 chunkSize = 8192; + QByteArray buffer; + buffer.resize(chunkSize); + + while (!file.atEnd()) { + auto bytesRead = file.read(buffer.data(), chunkSize); + if (bytesRead < 0) { + qCritical() << "Read error in file: " << fileInfo.filePath(); + return false; + } + + if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) { + qCritical() << "Write error in archive for: " << fileDest; + return false; + } + } + } + + return true; +} + +bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) +{ + std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); + auto entry = entry_ptr.get(); + if (!entry) { + qCritical() << "Failed to create archive entry"; + return false; + } + + auto fileDestUtf8 = fileDest.toUtf8(); + archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); + archive_entry_set_perm(entry, 0644); + + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_size(entry, data.size()); + + if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { + qCritical() << "Failed to write header for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + + if (archive_write_data(m_archive, data.constData(), data.size()) < 0) { + qCritical() << "Write error in archive for: " << fileDest << "-" << archive_error_string(m_archive); + return false; + } + return true; +} + +bool ArchiveWriter::addFile(ArchiveReader::File* f) +{ + return f->writeFile(m_archive, "", true); +} + +std::unique_ptr ArchiveWriter::createDiskWriter() +{ + int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | + ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS; + + std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { + if (a) { + archive_write_close(a); + archive_write_free(a); + } + }); + + archive* ext = extPtr.get(); + archive_write_disk_set_options(ext, flags); + archive_write_disk_set_standard_lookup(ext); + + return extPtr; +} +} // namespace MMCZip diff --git a/launcher/archive/ArchiveWriter.h b/launcher/archive/ArchiveWriter.h new file mode 100644 index 000000000..50858b517 --- /dev/null +++ b/launcher/archive/ArchiveWriter.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include "archive/ArchiveReader.h" + +struct archive; +namespace MMCZip { + +class ArchiveWriter { + public: + ArchiveWriter(const QString& archiveName); + virtual ~ArchiveWriter(); + + bool open(); + bool close(); + + bool addFile(const QString& fileName, const QString& fileDest); + bool addFile(const QString& fileDest, const QByteArray& data); + bool addFile(ArchiveReader::File* f); + + static std::unique_ptr createDiskWriter(); + + private: + struct archive* m_archive = nullptr; + QString m_filename; + QString m_format = "zip"; +}; +} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.cpp b/launcher/archive/ExportToZipTask.cpp new file mode 100644 index 000000000..bd3bc9032 --- /dev/null +++ b/launcher/archive/ExportToZipTask.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ExportToZipTask.h" + +#include + +#include "FileSystem.h" + +namespace MMCZip { +void ExportToZipTask::executeTask() +{ + setStatus("Adding files..."); + setProgress(0, m_files.length()); + m_buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); + connect(&m_buildZipWatcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); + m_buildZipWatcher.setFuture(m_buildZipFuture); +} + +auto ExportToZipTask::exportZip() -> ZipResult +{ + if (!m_dir.exists()) { + return ZipResult(tr("Folder doesn't exist")); + } + if (!m_output.open()) { + return ZipResult(tr("Could not create file")); + } + + for (auto fileName : m_extraFiles.keys()) { + if (m_buildZipFuture.isCanceled()) + return ZipResult(); + if (!m_output.addFile(fileName, m_extraFiles[fileName])) { + return ZipResult(tr("Could not add:") + fileName); + } + } + + for (const QFileInfo& file : m_files) { + if (m_buildZipFuture.isCanceled()) + return ZipResult(); + + auto absolute = file.absoluteFilePath(); + auto relative = m_dir.relativeFilePath(absolute); + setStatus("Compressing: " + relative); + setProgress(m_progress + 1, m_progressTotal); + if (m_followSymlinks) { + if (file.isSymLink()) + absolute = file.symLinkTarget(); + else + absolute = file.canonicalFilePath(); + } + + if (!m_excludeFiles.contains(relative) && !m_output.addFile(absolute, m_destinationPrefix + relative)) { + return ZipResult(tr("Could not read and compress %1").arg(relative)); + } + } + + if (!m_output.close()) { + return ZipResult(tr("A zip error occurred")); + } + return ZipResult(); +} + +void ExportToZipTask::finish() +{ + if (m_buildZipFuture.isCanceled()) { + FS::deletePath(m_outputPath); + emitAborted(); + } else if (auto result = m_buildZipFuture.result(); result.has_value()) { + FS::deletePath(m_outputPath); + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExportToZipTask::abort() +{ + if (m_buildZipFuture.isRunning()) { + m_buildZipFuture.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} +} // namespace MMCZip diff --git a/launcher/archive/ExportToZipTask.h b/launcher/archive/ExportToZipTask.h new file mode 100644 index 000000000..0c8329c93 --- /dev/null +++ b/launcher/archive/ExportToZipTask.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#include "archive/ArchiveWriter.h" +#include "tasks/Task.h" + +namespace MMCZip { +class ExportToZipTask : public Task { + Q_OBJECT + public: + ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : m_outputPath(outputPath) + , m_output(outputPath) + , m_dir(dir) + , m_files(files) + , m_destinationPrefix(destinationPrefix) + , m_followSymlinks(followSymlinks) + { + setAbortable(true); + }; + ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {}; + + virtual ~ExportToZipTask() = default; + + void setExcludeFiles(QStringList excludeFiles) { m_excludeFiles = excludeFiles; } + void addExtraFile(QString fileName, QByteArray data) { m_extraFiles.insert(fileName, data); } + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult exportZip(); + void finish(); + + private: + QString m_outputPath; + ArchiveWriter m_output; + QDir m_dir; + QFileInfoList m_files; + QString m_destinationPrefix; + bool m_followSymlinks; + QStringList m_excludeFiles; + QHash m_extraFiles; + + QFuture m_buildZipFuture; + QFutureWatcher m_buildZipWatcher; +}; +} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp new file mode 100644 index 000000000..acbcd39d5 --- /dev/null +++ b/launcher/archive/ExtractZipTask.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ExtractZipTask.h" +#include +#include "FileSystem.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" + +namespace MMCZip { + +void ExtractZipTask::executeTask() +{ + m_zipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); + connect(&m_zipWatcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); + m_zipWatcher.setFuture(m_zipFuture); +} + +auto ExtractZipTask::extractZip() -> ZipResult +{ + auto target = m_outputDir.absolutePath(); + auto target_top_dir = QUrl::fromLocalFile(target); + + QStringList extracted; + + qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target; + if (!m_input.collectFiles()) { + return ZipResult(tr("Failed to enumerate files in archive")); + } + if (m_input.getFiles().isEmpty()) { + logWarning(tr("Extracting empty archives seems odd...")); + return ZipResult(); + } + + auto extPtr = ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + setStatus("Extracting files..."); + setProgress(0, m_input.getFiles().count()); + ZipResult result; + auto fileName = m_input.getZipName(); + if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { + if (m_zipFuture.isCanceled()) + return false; + setProgress(m_progress + 1, m_progressTotal); + QString file_name = f->filename(); + if (!file_name.startsWith(m_subdirectory)) { + f->skip(); + return true; + } + + auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); + auto original_name = relative_file_name; + setStatus("Unpacking: " + relative_file_name); + + // Fix subdirs/files ending with a / getting transformed into absolute paths + if (relative_file_name.startsWith('/')) + relative_file_name = relative_file_name.mid(1); + + // Fix weird "folders with a single file get squashed" thing + QString sub_path; + if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { + sub_path = relative_file_name.section('/', 0, -2) + '/'; + FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); + + relative_file_name = relative_file_name.split('/').last(); + } + + QString target_file_path; + if (relative_file_name.isEmpty()) { + target_file_path = target + '/'; + } else { + target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); + if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) + target_file_path += '/'; + } + + if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { + result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") + .arg(relative_file_name, target)); + return false; + } + + if (!f->writeFile(ext, target_file_path)) { + result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); + return false; + } + extracted.append(target_file_path); + + qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; + return true; + })) { + FS::removeFiles(extracted); + return result.has_value() ? result : ZipResult(tr("Failed to parse file %1").arg(fileName)); + } + return ZipResult(); +} + +void ExtractZipTask::finish() +{ + if (m_zipFuture.isCanceled()) { + emitAborted(); + } else if (auto result = m_zipFuture.result(); result.has_value()) { + emitFailed(result.value()); + } else { + emitSucceeded(); + } +} + +bool ExtractZipTask::abort() +{ + if (m_zipFuture.isRunning()) { + m_zipFuture.cancel(); + // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur + // immediately. + return true; + } + return false; +} + +} // namespace MMCZip diff --git a/launcher/archive/ExtractZipTask.h b/launcher/archive/ExtractZipTask.h new file mode 100644 index 000000000..03c391aee --- /dev/null +++ b/launcher/archive/ExtractZipTask.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include "archive/ArchiveReader.h" +#include "tasks/Task.h" + +namespace MMCZip { + +class ExtractZipTask : public Task { + Q_OBJECT + public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : m_input(input), m_outputDir(outputDir), m_subdirectory(subdirectory) + {} + virtual ~ExtractZipTask() = default; + + using ZipResult = std::optional; + + protected: + virtual void executeTask() override; + bool abort() override; + + ZipResult extractZip(); + void finish(); + + private: + ArchiveReader m_input; + QDir m_outputDir; + QString m_subdirectory; + + QFuture m_zipFuture; + QFutureWatcher m_zipWatcher; +}; +} // namespace MMCZip diff --git a/launcher/console/Console.h b/launcher/console/Console.h new file mode 100644 index 000000000..097ab4a6b --- /dev/null +++ b/launcher/console/Console.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#endif + +namespace console { + +inline bool isConsole() +{ +#if defined Q_OS_WIN32 + DWORD procIDs[2]; + DWORD maxCount = 2; + DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount); + return result > 1; +#else + if (isatty(fileno(stdout))) { + return true; + } + return false; +#endif +} + +} // namespace console diff --git a/launcher/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp similarity index 78% rename from launcher/WindowsConsole.cpp rename to launcher/console/WindowsConsole.cpp index 83cad5afa..4a0eb3d3d 100644 --- a/launcher/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -16,13 +16,18 @@ * */ +#include "WindowsConsole.h" +#include + #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif +#include + #include #include #include -#include +#include #include void RedirectHandle(DWORD handle, FILE* stream, const char* mode) @@ -126,3 +131,29 @@ bool AttachWindowsConsole() return false; } + +std::error_code EnableAnsiSupport() +{ + // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected + HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0); + if (console_handle == INVALID_HANDLE_VALUE) { + return std::error_code(GetLastError(), std::system_category()); + } + + // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode + DWORD console_mode; + if (0 == GetConsoleMode(console_handle, &console_mode)) { + return std::error_code(GetLastError(), std::system_category()); + } + + // VT processing not already enabled? + if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) { + // https://docs.microsoft.com/en-us/windows/console/setconsolemode + if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { + return std::error_code(GetLastError(), std::system_category()); + } + } + + return {}; +} diff --git a/launcher/WindowsConsole.h b/launcher/console/WindowsConsole.h similarity index 93% rename from launcher/WindowsConsole.h rename to launcher/console/WindowsConsole.h index ab53864b4..4c1f3ee28 100644 --- a/launcher/WindowsConsole.h +++ b/launcher/console/WindowsConsole.h @@ -21,5 +21,8 @@ #pragma once +#include + void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); +std::error_code EnableAnsiSupport(); diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index bdf173ebc..1494fa8cc 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -37,27 +37,14 @@ #include #if defined Q_OS_WIN32 -#include "WindowsConsole.h" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include "console/WindowsConsole.h" #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { @@ -104,11 +91,11 @@ void FileLinkApp::joinServer(QString server) in.setDevice(&socket); - connect(&socket, &QLocalSocket::connected, this, [&]() { qDebug() << "connected to server"; }); + connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); - connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { + connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) { m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: @@ -132,7 +119,7 @@ void FileLinkApp::joinServer(QString server) } }); - connect(&socket, &QLocalSocket::disconnected, this, [&]() { + connect(&socket, &QLocalSocket::disconnected, this, [this]() { qDebug() << "disconnected from server, should exit"; m_status = Succeeded; exit(); diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index e4157ea2d..6b028f52a 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -37,34 +37,34 @@ #include "IconList.h" #include #include -#include #include #include #include +#include #include #include #include "icons/IconUtils.h" #define MAX_SIZE 1024 -IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent) +IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent) { QSet builtinNames; // add builtin icons - for (auto& builtinPath : builtinPaths) { - QDir instance_icons(builtinPath); - auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); - for (auto file_info : file_info_list) { - builtinNames.insert(file_info.completeBaseName()); + for (const auto& builtinPath : builtinPaths) { + QDir instanceIcons(builtinPath); + auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); + for (const auto& fileInfo : fileInfoList) { + builtinNames.insert(fileInfo.completeBaseName()); } } - for (auto& builtinName : builtinNames) { + for (const auto& builtinName : builtinNames) { addThemeIcon(builtinName); } m_watcher.reset(new QFileSystemWatcher()); - is_watching = false; + m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); @@ -77,91 +77,129 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren void IconList::sortIconList() { qDebug() << "Sorting icon list..."; - std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; }); + std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) { + bool aIsSubdir = a.m_key.contains(QDir::separator()); + bool bIsSubdir = b.m_key.contains(QDir::separator()); + if (aIsSubdir != bIsSubdir) { + return !aIsSubdir; // root-level icons come first + } + return a.m_key.localeAwareCompare(b.m_key) < 0; + }); reindex(); } +// Helper function to add directories recursively +bool IconList::addPathRecursively(const QString& path) +{ + QDir dir(path); + if (!dir.exists()) + return false; + + // Add the directory itself + bool watching = m_watcher->addPath(path); + + // Add all subdirectories + QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& entry : entries) { + if (addPathRecursively(entry.absoluteFilePath())) { + watching = true; + } + } + return watching; +} + +QStringList IconList::getIconFilePaths() const +{ + QStringList iconFiles{}; + QStringList directories{ m_dir.absolutePath() }; + while (!directories.isEmpty()) { + QString first = directories.takeFirst(); + QDir dir(first); + for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { + if (fileInfo.isDir()) + directories.push_back(fileInfo.absoluteFilePath()); + else + iconFiles.push_back(fileInfo.absoluteFilePath()); + } + } + return iconFiles; +} + +QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) +{ + if (iconFile.dir() == iconsDir) + return iconFile.completeBaseName(); + + constexpr auto delimiter = " » "; + QString relativePathWithoutExtension = + iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); + return relativePathWithoutExtension.replace(QDir::separator(), delimiter); +} + +/// Split into a separate function because the preprocessing impedes readability +QSet toStringSet(const QList& list) +{ + QSet set(list.begin(), list.end()); + return set; +} + void IconList::directoryChanged(const QString& path) { - QDir new_dir(path); - if (m_dir.absolutePath() != new_dir.absolutePath()) { + QDir newDir(path); + if (m_dir.absolutePath() != newDir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); - if (is_watching) + if (m_isWatching) stopWatching(); startWatching(); } - if (!m_dir.exists()) - if (!FS::ensureFolderPathExists(m_dir.absolutePath())) - return; + if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) + return; m_dir.refresh(); - auto new_list = m_dir.entryList(QDir::Files, QDir::Name); - for (auto it = new_list.begin(); it != new_list.end(); it++) { - QString& foo = (*it); - foo = m_dir.filePath(foo); - } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QSet new_set(new_list.begin(), new_list.end()); -#else - auto new_set = new_list.toSet(); -#endif - QList current_list; - for (auto& it : icons) { + const QStringList newFileNamesList = getIconFilePaths(); + const QSet newSet = toStringSet(newFileNamesList); + QSet currentSet; + for (const MMCIcon& it : m_icons) { if (!it.has(IconType::FileBased)) continue; - current_list.push_back(it.m_images[IconType::FileBased].filename); + QFileInfo icon(it.getFilePath()); + currentSet.insert(icon.absoluteFilePath()); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QSet current_set(current_list.begin(), current_list.end()); -#else - QSet current_set = current_list.toSet(); -#endif - - QSet to_remove = current_set; - to_remove -= new_set; + QSet toRemove = currentSet - newSet; + QSet toAdd = newSet - currentSet; - QSet to_add = new_set; - to_add -= current_set; - - for (auto remove : to_remove) { - qDebug() << "Removing " << remove; - QFileInfo rmfile(remove); - QString key = rmfile.completeBaseName(); - - QString suffix = rmfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = rmfile.fileName(); + for (const QString& removedPath : toRemove) { + qDebug() << "Removing icon " << removedPath; + QFileInfo removedFile(removedPath); + QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); int idx = getIconIndex(key); if (idx == -1) continue; - icons[idx].remove(IconType::FileBased); - if (icons[idx].type() == IconType::ToBeDeleted) { + m_icons[idx].remove(FileBased); + if (m_icons[idx].type() == ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); - icons.remove(idx); + m_icons.remove(idx); reindex(); endRemoveRows(); } else { dataChanged(index(idx), index(idx)); } - m_watcher->removePath(remove); + m_watcher->removePath(removedPath); emit iconUpdated(key); } - for (auto add : to_add) { - qDebug() << "Adding " << add; + for (const QString& addedPath : toAdd) { + qDebug() << "Adding icon " << addedPath; - QFileInfo addfile(add); - QString key = addfile.completeBaseName(); + QFileInfo addfile(addedPath); + QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); + QString name = formatName(m_dir, addfile); - QString suffix = addfile.suffix(); - // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (!IconUtils::isIconSuffix(suffix)) - key = addfile.fileName(); - - if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { - m_watcher->addPath(add); + if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { + m_watcher->addPath(addedPath); emit iconUpdated(key); } } @@ -171,24 +209,30 @@ void IconList::directoryChanged(const QString& path) void IconList::fileChanged(const QString& path) { - qDebug() << "Checking " << path; + qDebug() << "Checking icon " << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.completeBaseName(); + QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) return; - QIcon icon(path); - if (!icon.availableSizes().size()) + QIcon icon; + // special handling for jpg and jpeg to go through pixmap to keep the size constant + if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { + icon.addPixmap(QPixmap(path)); + } else { + icon.addFile(path); + } + if (icon.availableSizes().empty()) return; - icons[idx].m_images[IconType::FileBased].icon = icon; + m_icons[idx].m_images[IconType::FileBased].icon = icon; dataChanged(index(idx), index(idx)); emit iconUpdated(key); } -void IconList::SettingChanged(const Setting& setting, QVariant value) +void IconList::SettingChanged(const Setting& setting, const QVariant& value) { if (setting.id() != "IconsDir") return; @@ -200,8 +244,8 @@ void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); - is_watching = m_watcher->addPath(abs_path); - if (is_watching) { + m_isWatching = addPathRecursively(abs_path); + if (m_isWatching) { qDebug() << "Started watching " << abs_path; } else { qDebug() << "Failed to start watching " << abs_path; @@ -212,7 +256,7 @@ void IconList::stopWatching() { m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); - is_watching = false; + m_isWatching = false; } QStringList IconList::mimeTypes() const @@ -242,7 +286,7 @@ bool IconList::dropMimeData(const QMimeData* data, if (data->hasUrls()) { auto urls = data->urls(); QStringList iconFiles; - for (auto url : urls) { + for (const auto& url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; @@ -263,33 +307,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const QVariant IconList::data(const QModelIndex& index, int role) const { if (!index.isValid()) - return QVariant(); + return {}; int row = index.row(); - if (row < 0 || row >= icons.size()) - return QVariant(); + if (row < 0 || row >= m_icons.size()) + return {}; switch (role) { case Qt::DecorationRole: - return icons[row].icon(); + return m_icons[row].icon(); case Qt::DisplayRole: - return icons[row].name(); + return m_icons[row].name(); case Qt::UserRole: - return icons[row].m_key; + return m_icons[row].m_key; default: - return QVariant(); + return {}; } } int IconList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : icons.size(); + return parent.isValid() ? 0 : m_icons.size(); } void IconList::installIcons(const QStringList& iconFiles) { - for (QString file : iconFiles) + for (const QString& file : iconFiles) installIcon(file, {}); } @@ -312,12 +356,13 @@ bool IconList::iconFileExists(const QString& key) const return iconEntry && iconEntry->has(IconType::FileBased); } +/// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) return nullptr; - return &icons[iconIdx]; + return &m_icons[iconIdx]; } bool IconList::deleteIcon(const QString& key) @@ -332,22 +377,22 @@ bool IconList::trashIcon(const QString& key) bool IconList::addThemeIcon(const QString& key) { - auto iter = name_index.find(key); - if (iter != name_index.end()) { - auto& oldOne = icons[*iter]; + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) { + auto& oldOne = m_icons[*iter]; oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = key; mmc_icon.m_key = key; mmc_icon.replace(Builtin, key); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; + m_icons.push_back(mmc_icon); + m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; @@ -356,25 +401,32 @@ bool IconList::addThemeIcon(const QString& key) bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) { // replace the icon even? is the input valid? - QIcon icon(path); + QIcon icon; + // special handling for jpg and jpeg to go through pixmap to keep the size constant + if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { + icon.addPixmap(QPixmap(path)); + } else { + icon.addFile(path); + } + if (icon.isNull()) return false; - auto iter = name_index.find(key); - if (iter != name_index.end()) { - auto& oldOne = icons[*iter]; + auto iter = m_nameIndex.find(key); + if (iter != m_nameIndex.end()) { + auto& oldOne = m_icons[*iter]; oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); + beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = name; mmc_icon.m_key = key; mmc_icon.replace(type, icon, path); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; + m_icons.push_back(mmc_icon); + m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; @@ -389,33 +441,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for void IconList::reindex() { - name_index.clear(); - int i = 0; - for (auto& iter : icons) { - name_index[iter.m_key] = i; - i++; + m_nameIndex.clear(); + for (int i = 0; i < m_icons.size(); i++) { + m_nameIndex[m_icons[i].m_key] = i; + emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model } } QIcon IconList::getIcon(const QString& key) const { - int icon_index = getIconIndex(key); + int iconIndex = getIconIndex(key); - if (icon_index != -1) - return icons[icon_index].icon(); + if (iconIndex != -1) + return m_icons[iconIndex].icon(); - // Fallback for icons that don't exist. - icon_index = getIconIndex("grass"); + // Fallback for icons that don't exist.b + iconIndex = getIconIndex("grass"); - if (icon_index != -1) - return icons[icon_index].icon(); - return QIcon(); + if (iconIndex != -1) + return m_icons[iconIndex].icon(); + return {}; } int IconList::getIconIndex(const QString& key) const { - auto iter = name_index.find(key == "default" ? "grass" : key); - if (iter != name_index.end()) + auto iter = m_nameIndex.find(key == "default" ? "grass" : key); + if (iter != m_nameIndex.end()) return *iter; return -1; @@ -425,3 +476,15 @@ QString IconList::getDirectory() const { return m_dir.absolutePath(); } + +/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. +QString IconList::iconDirectory(const QString& key) const +{ + for (const auto& mmcIcon : m_icons) { + if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) { + QFileInfo iconFile(mmcIcon.getFilePath()); + return iconFile.dir().path(); + } + } + return getDirectory(); +} diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index 553946c42..d2f904448 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -51,7 +51,7 @@ class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: - explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0); + explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); virtual ~IconList() {}; QIcon getIcon(const QString& key) const; @@ -72,6 +72,7 @@ class IconList : public QAbstractListModel { bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; + QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); @@ -91,18 +92,20 @@ class IconList : public QAbstractListModel { IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); + bool addPathRecursively(const QString& path); + QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); protected slots: void fileChanged(const QString& path); - void SettingChanged(const Setting& setting, QVariant value); + void SettingChanged(const Setting& setting, const QVariant& value); private: shared_qobject_ptr m_watcher; - bool is_watching; - QMap name_index; - QVector icons; + bool m_isWatching; + QMap m_nameIndex; + QList m_icons; QDir m_dir; }; diff --git a/launcher/include/base.pch.hpp b/launcher/include/base.pch.hpp new file mode 100644 index 000000000..c858857f9 --- /dev/null +++ b/launcher/include/base.pch.hpp @@ -0,0 +1,17 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_BASE_HEADERS_H +#define PRISM_PRECOMPILED_BASE_HEADERS_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#endif // PRISM_PRECOMPILED_BASE_HEADERS_H diff --git a/launcher/include/qtcore.pch.hpp b/launcher/include/qtcore.pch.hpp new file mode 100644 index 000000000..d7c24ddb6 --- /dev/null +++ b/launcher/include/qtcore.pch.hpp @@ -0,0 +1,59 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_QTCORE_HEADERS_H +#define PRISM_PRECOMPILED_QTCORE_HEADERS_H + +#include +#include +#include +#include + +#include + +#include + +// collections +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +#include +#include +#include + +#endif // PRISM_PRECOMPILED_QTCORE_HEADERS_H diff --git a/launcher/include/qtgui.pch.hpp b/launcher/include/qtgui.pch.hpp new file mode 100644 index 000000000..fb57cb33a --- /dev/null +++ b/launcher/include/qtgui.pch.hpp @@ -0,0 +1,47 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_QTGUI_HEADERS_H +#define PRISM_PRECOMPILED_QTGUI_HEADERS_H + +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#endif // PRISM_PRECOMPILED_GUI_HEADERS_H diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in deleted file mode 100644 index acbce9650..000000000 --- a/launcher/install_prereqs.cmake.in +++ /dev/null @@ -1,26 +0,0 @@ -set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") -file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") -function(gp_resolved_file_type_override resolved_file type_var) - if(resolved_file MATCHES "^/(usr/)?lib/libQt") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libxcb-") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libicu") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libpng") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libproxy") - set(${type_var} other PARENT_SCOPE) - elseif((resolved_file MATCHES "^/(usr/)?lib(.+)?/libstdc\\+\\+") AND (UNIX AND NOT APPLE)) - set(${type_var} other PARENT_SCOPE) - endif() -endfunction() - -set(gp_tool "@CMAKE_GP_TOOL@") -set(gp_cmd_paths ${gp_cmd_paths} - "@CMAKE_GP_CMD_PATHS@" -) - -include(BundleUtilities) -fixup_bundle("@APPS@" "${QTPLUGINS}" "@DIRS@") - diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 07b5d7b40..23bd1b73e 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -84,7 +84,7 @@ void JavaChecker::executeTask() process->setProcessEnvironment(CleanEnviroment()); qDebug() << "Running java checker:" << m_path << args.join(" "); - connect(process.get(), QOverload::of(&QProcess::finished), this, &JavaChecker::finished); + connect(process.get(), &QProcess::finished, this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady); connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady); @@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QMap results; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); -#else - QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); -#endif for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux @@ -149,11 +145,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) continue; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto parts = line.split('=', Qt::SkipEmptyParts); -#else - auto parts = line.split('=', QString::SkipEmptyParts); -#endif if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 8e97e0e14..30cb77e08 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -21,7 +21,7 @@ #include "BaseVersion.h" #include "StringUtils.h" -bool JavaInstall::operator<(const JavaInstall& rhs) +bool JavaInstall::operator<(const JavaInstall& rhs) const { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if (archCompare != 0) @@ -35,17 +35,17 @@ bool JavaInstall::operator<(const JavaInstall& rhs) return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } -bool JavaInstall::operator==(const JavaInstall& rhs) +bool JavaInstall::operator==(const JavaInstall& rhs) const { return arch == rhs.arch && id == rhs.id && path == rhs.path; } -bool JavaInstall::operator>(const JavaInstall& rhs) +bool JavaInstall::operator>(const JavaInstall& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } -bool JavaInstall::operator<(BaseVersion& a) +bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); @@ -54,7 +54,7 @@ bool JavaInstall::operator<(BaseVersion& a) } } -bool JavaInstall::operator>(BaseVersion& a) +bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 7d8d392fa..d8fd477fd 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -24,17 +24,17 @@ struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} - virtual QString descriptor() override { return id.toString(); } + virtual QString descriptor() const override { return id.toString(); } - virtual QString name() override { return id.toString(); } + virtual QString name() const override { return id.toString(); } virtual QString typeString() const override { return arch; } - virtual bool operator<(BaseVersion& a) override; - virtual bool operator>(BaseVersion& a) override; - bool operator<(const JavaInstall& rhs); - bool operator==(const JavaInstall& rhs); - bool operator>(const JavaInstall& rhs); + virtual bool operator<(BaseVersion& a) const override; + virtual bool operator>(BaseVersion& a) const override; + bool operator<(const JavaInstall& rhs) const; + bool operator==(const JavaInstall& rhs) const; + bool operator>(const JavaInstall& rhs) const; JavaVersion id; QString arch; diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index b77f17b28..c68c2a3be 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); - [[nodiscard]] Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp index 2d68f55c8..115baa9e5 100644 --- a/launcher/java/JavaMetadata.cpp +++ b/launcher/java/JavaMetadata.cpp @@ -52,33 +52,33 @@ MetadataPtr parseJavaMeta(const QJsonObject& in) { auto meta = std::make_shared(); - meta->m_name = Json::ensureString(in, "name", ""); - meta->vendor = Json::ensureString(in, "vendor", ""); - meta->url = Json::ensureString(in, "url", ""); - meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); - meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", "")); - meta->packageType = Json::ensureString(in, "packageType", ""); - meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); + meta->m_name = in["name"].toString(""); + meta->vendor = in["vendor"].toString(""); + meta->url = in["url"].toString(""); + meta->releaseTime = timeFromS3Time(in["releaseTime"].toString("")); + meta->downloadType = parseDownloadType(in["downloadType"].toString("")); + meta->packageType = in["packageType"].toString(""); + meta->runtimeOS = in["runtimeOS"].toString("unknown"); if (in.contains("checksum")) { auto obj = Json::requireObject(in, "checksum"); - meta->checksumHash = Json::ensureString(obj, "hash", ""); - meta->checksumType = Json::ensureString(obj, "type", ""); + meta->checksumHash = obj["hash"].toString(""); + meta->checksumType = obj["type"].toString(""); } if (in.contains("version")) { auto obj = Json::requireObject(in, "version"); - auto name = Json::ensureString(obj, "name", ""); - auto major = Json::ensureInteger(obj, "major", 0); - auto minor = Json::ensureInteger(obj, "minor", 0); - auto security = Json::ensureInteger(obj, "security", 0); - auto build = Json::ensureInteger(obj, "build", 0); + auto name = obj["name"].toString(""); + auto major = obj["major"].toInteger(); + auto minor = obj["minor"].toInteger(); + auto security = obj["security"].toInteger(); + auto build = obj["build"].toInteger(); meta->version = JavaVersion(major, minor, security, build, name); } return meta; } -bool Metadata::operator<(const Metadata& rhs) +bool Metadata::operator<(const Metadata& rhs) const { auto id = version; if (id < rhs.version) { @@ -97,17 +97,17 @@ bool Metadata::operator<(const Metadata& rhs) return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; } -bool Metadata::operator==(const Metadata& rhs) +bool Metadata::operator==(const Metadata& rhs) const { return version == rhs.version && m_name == rhs.m_name; } -bool Metadata::operator>(const Metadata& rhs) +bool Metadata::operator>(const Metadata& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } -bool Metadata::operator<(BaseVersion& a) +bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); @@ -116,7 +116,7 @@ bool Metadata::operator<(BaseVersion& a) } } -bool Metadata::operator>(BaseVersion& a) +bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h index 77a42fd78..0757a6935 100644 --- a/launcher/java/JavaMetadata.h +++ b/launcher/java/JavaMetadata.h @@ -32,17 +32,17 @@ enum class DownloadType { Manifest, Archive, Unknown }; class Metadata : public BaseVersion { public: - virtual QString descriptor() override { return version.toString(); } + virtual QString descriptor() const override { return version.toString(); } - virtual QString name() override { return m_name; } + virtual QString name() const override { return m_name; } virtual QString typeString() const override { return vendor; } - virtual bool operator<(BaseVersion& a) override; - virtual bool operator>(BaseVersion& a) override; - bool operator<(const Metadata& rhs); - bool operator==(const Metadata& rhs); - bool operator>(const Metadata& rhs); + virtual bool operator<(BaseVersion& a) const override; + virtual bool operator>(BaseVersion& a) const override; + bool operator<(const Metadata& rhs) const; + bool operator==(const Metadata& rhs) const; + bool operator>(const Metadata& rhs) const; QString m_name; QString vendor; @@ -61,4 +61,4 @@ DownloadType parseDownloadType(QString javaDownload); QString downloadTypeToString(DownloadType javaDownload); MetadataPtr parseJavaMeta(const QJsonObject& libObj); -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index f3200428e..25fe1caa8 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -365,13 +365,13 @@ QList JavaUtils::FindJavaPaths() javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, libraryJVMJavas) { + for (const QString& java : libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, systemLibraryJVMJavas) { + for (const QString& java : systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -379,16 +379,25 @@ QList JavaUtils::FindJavaPaths() auto home = qEnvironmentVariable("HOME"); // javas downloaded by sdkman - QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); - QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, sdkmanJavas) { - javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); + QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); + QDir sdkmanJavaDir(FS::PathCombine(sdkmanDir, "candidates/java")); + QStringList sdkmanJavas = sdkmanJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : sdkmanJavas) { + javas.append(sdkmanJavaDir.absolutePath() + "/" + java + "/bin/java"); + } + + // javas downloaded by asdf + QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); + QDir asdfJavaDir(FS::PathCombine(asdfDataDir, "installs/java")); + QStringList asdfJavas = asdfJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& java : asdfJavas) { + javas.append(asdfJavaDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, userLibraryJVMJavas) { + for (const QString& java : userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -405,7 +414,7 @@ QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); - auto scanJavaDir = [&]( + auto scanJavaDir = [&javas]( const QString& dirPath, const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); @@ -424,7 +433,7 @@ QList JavaUtils::FindJavaPaths() }; // java installed in a snap is installed in the standard directory, but underneath $SNAP auto snap = qEnvironmentVariable("SNAP"); - auto scanJavaDirs = [&](const QString& dirPath) { + auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) { scanJavaDir(dirPath); if (!snap.isNull()) { scanJavaDir(snap + dirPath); @@ -442,9 +451,15 @@ QList JavaUtils::FindJavaPaths() QString fileName = info.fileName(); return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); }; + // AOSC OS's locations for openjdk + auto aoscFilter = [](const QFileInfo& info) { + QString fileName = info.fileName(); + return fileName == "java" || fileName.startsWith("java-"); + }; scanJavaDir("/usr/lib64", gentooFilter); scanJavaDir("/usr/lib", gentooFilter); scanJavaDir("/opt", gentooFilter); + scanJavaDir("/usr/lib", aoscFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt @@ -462,9 +477,14 @@ QList JavaUtils::FindJavaPaths() // javas downloaded by IntelliJ scanJavaDirs(FS::PathCombine(home, ".jdks")); // javas downloaded by sdkman - scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); + QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); + scanJavaDirs(FS::PathCombine(sdkmanDir, "candidates/java")); + // javas downloaded by asdf + QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); + scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); // javas downloaded by gradle (toolchains) - scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); + QString gradleUserHome = qEnvironmentVariable("GRADLE_USER_HOME", FS::PathCombine(home, ".gradle")); + scanJavaDirs(FS::PathCombine(gradleUserHome, "jdks")); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); @@ -495,7 +515,7 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { QStringList processpaths; -#if defined(Q_OS_OSX) +#if defined(Q_OS_MACOS) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) @@ -546,12 +566,12 @@ QStringList getPrismJavaBundle() { QList javas; - auto scanDir = [&](QString prefix) { + auto scanDir = [&javas](QString prefix) { javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); }; - auto scanJavaDir = [&](const QString& dirPath) { + auto scanJavaDir = [scanDir](const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index bca50f2c9..fef573c16 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString) QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { - pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withOne( + "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withOne; } else { - pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withoutOne( + "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withoutOne; } auto match = pattern.match(m_string); @@ -59,7 +63,7 @@ bool JavaVersion::isModular() const return m_parseable && m_major >= 9; } -bool JavaVersion::operator<(const JavaVersion& rhs) +bool JavaVersion::operator<(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { auto major = m_major; @@ -97,7 +101,7 @@ bool JavaVersion::operator<(const JavaVersion& rhs) return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } -bool JavaVersion::operator==(const JavaVersion& rhs) +bool JavaVersion::operator==(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; @@ -105,7 +109,7 @@ bool JavaVersion::operator==(const JavaVersion& rhs) return m_string == rhs.m_string; } -bool JavaVersion::operator>(const JavaVersion& rhs) +bool JavaVersion::operator>(const JavaVersion& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index c070bdeec..143ddd262 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -20,9 +20,9 @@ class JavaVersion { JavaVersion& operator=(const QString& rhs); - bool operator<(const JavaVersion& rhs); - bool operator==(const JavaVersion& rhs); - bool operator>(const JavaVersion& rhs); + bool operator<(const JavaVersion& rhs) const; + bool operator==(const JavaVersion& rhs) const; + bool operator>(const JavaVersion& rhs) const; bool requiresPermGen() const; bool defaultsToUtf8() const; diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index bb7cc568d..c60908cec 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -16,12 +16,11 @@ * along with this program. If not, see . */ #include "java/download/ArchiveDownloadTask.h" -#include #include -#include "MMCZip.h" #include "Application.h" -#include "Untar.h" +#include "archive/ArchiveReader.h" +#include "archive/ExtractZipTask.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "tasks/Task.h" @@ -55,6 +54,7 @@ void ArchiveDownloadTask::executeTask() connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); + connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); @@ -66,68 +66,45 @@ void ArchiveDownloadTask::executeTask() void ArchiveDownloadTask::extractJava(QString input) { setStatus(tr("Extracting Java")); - if (input.endsWith("tar")) { - setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); - QFile in(input); - if (!in.open(QFile::ReadOnly)) { - emitFailed(tr("Unable to open supplied tar file.")); - return; - } - if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) { - emitFailed(tr("Unable to extract supplied tar file.")); - return; - } - emitSucceeded(); + + MMCZip::ArchiveReader zip(input); + if (!zip.collectFiles()) { + emitFailed(tr("Unable to open supplied zip file.")); return; - } else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) { - setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); - if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) { - emitFailed(tr("Unable to extract supplied tar file.")); - return; - } - emitSucceeded(); + } + auto files = zip.getFiles(); + if (files.isEmpty()) { + emitFailed(tr("No files were found in the supplied zip file.")); return; - } else if (input.endsWith("zip")) { - auto zip = std::make_shared(input); - if (!zip->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied zip file.")); - return; - } - auto files = zip->getFileNameList(); - if (files.isEmpty()) { - emitFailed(tr("No files were found in the supplied zip file.")); - return; - } - m_task = makeShared(zip, m_final_path, files[0]); - - auto progressStep = std::make_shared(); - connect(m_task.get(), &Task::finished, this, [this, progressStep] { - progressStep->state = TaskStepState::Succeeded; - stepProgress(*progressStep); - }); + } + auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts); + m_task = makeShared(input, m_final_path, firstFolderParts.value(0)); - connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); - connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); - connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { - progressStep->state = TaskStepState::Failed; - stepProgress(*progressStep); - emitFailed(reason); - }); - connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + auto progressStep = std::make_shared(); + connect(m_task.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); - connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { - progressStep->update(current, total); - stepProgress(*progressStep); - }); - connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { - progressStep->status = status; - stepProgress(*progressStep); - }); - m_task->start(); - return; - } + connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); + connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); - emitFailed(tr("Could not determine archive type!")); + connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); + return; } bool ArchiveDownloadTask::abort() @@ -135,7 +112,6 @@ bool ArchiveDownloadTask::abort() auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); - emitAborted(); return aborted; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h index 1db33763a..cfcdf9dcf 100644 --- a/launcher/java/download/ArchiveDownloadTask.h +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -28,7 +28,7 @@ class ArchiveDownloadTask : public Task { ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ArchiveDownloadTask() = default; - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; @@ -42,4 +42,4 @@ class ArchiveDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp index 05882b89d..cbb76bac6 100644 --- a/launcher/java/download/ManifestDownloadTask.cpp +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -41,7 +41,7 @@ void ManifestDownloadTask::executeTask() auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); auto files = std::make_shared(); - auto action = Net::Download::makeByteArray(m_url, files); + auto action = Net::Download::makeByteArray(m_url, files.get()); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { @@ -77,27 +77,27 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) // valid json doc, begin making jre spot FS::ensureFolderPathExists(m_final_path); std::vector toDownload; - auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); + auto list = doc.object()["files"].toObject(); for (const auto& paths : list.keys()) { auto file = FS::PathCombine(m_final_path, paths); - const QJsonObject& meta = Json::ensureObject(list, paths); - auto type = Json::ensureString(meta, "type"); + const QJsonObject& meta = list[paths].toObject(); + auto type = meta["type"].toString(); if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { // this is *nix only ! - auto path = Json::ensureString(meta, "target"); + auto path = meta["target"].toString(); if (!path.isEmpty()) { QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? - auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw"); - auto isExec = Json::ensureBoolean(meta, QStringLiteral("executable"), false); - auto url = Json::ensureString(raw, "url"); + auto raw = meta["downloads"].toObject()["raw"].toObject(); + auto isExec = meta["executable"].toBool(); + auto url = raw["url"].toString(); if (!url.isEmpty() && QUrl(url).isValid()) { - auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec }; + auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec }; toDownload.push_back(f); } } @@ -134,4 +134,4 @@ bool ManifestDownloadTask::abort() emitAborted(); return aborted; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h index ae9e0d0ed..e68c8236f 100644 --- a/launcher/java/download/ManifestDownloadTask.h +++ b/launcher/java/download/ManifestDownloadTask.h @@ -29,7 +29,7 @@ class ManifestDownloadTask : public Task { ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ManifestDownloadTask() = default; - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; @@ -43,4 +43,4 @@ class ManifestDownloadTask : public Task { QString m_checksum_hash; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/SymlinkTask.cpp b/launcher/java/download/SymlinkTask.cpp index 843c7caa9..9bbd50c63 100644 --- a/launcher/java/download/SymlinkTask.cpp +++ b/launcher/java/download/SymlinkTask.cpp @@ -78,4 +78,4 @@ void SymlinkTask::executeTask() } } -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/java/download/SymlinkTask.h b/launcher/java/download/SymlinkTask.h index 88cb20dd7..e38323eae 100644 --- a/launcher/java/download/SymlinkTask.h +++ b/launcher/java/download/SymlinkTask.h @@ -33,4 +33,4 @@ class SymlinkTask : public Task { QString m_path; Task::Ptr m_task; }; -} // namespace Java \ No newline at end of file +} // namespace Java diff --git a/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h index d49d7545b..80dcd31e9 100644 --- a/launcher/launch/LaunchStep.h +++ b/launcher/launch/LaunchStep.h @@ -28,8 +28,8 @@ class LaunchStep : public Task { virtual ~LaunchStep() = default; signals: - void logLines(QStringList lines, MessageLevel::Enum level); - void logLine(QString line, MessageLevel::Enum level); + void logLines(QStringList lines, MessageLevel level); + void logLine(QString line, MessageLevel level); void readyForLaunch(); void progressReportingRequest(); diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 4b93d2077..6aaf2af96 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,12 +37,12 @@ #include "launch/LaunchTask.h" #include +#include #include #include #include -#include -#include #include +#include #include "MessageLevel.h" #include "tasks/Task.h" @@ -51,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -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) { @@ -203,8 +203,8 @@ shared_qobject_ptr LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); - m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); - m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" @@ -214,31 +214,77 @@ shared_qobject_ptr LaunchTask::getLogModel() return m_logModel; } -void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel) +bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level) { - for (auto& line : lines) { - onLogLine(line, defaultLevel); + LogParser* parser; + switch (static_cast(level)) { + case MessageLevel::StdErr: + parser = &m_stderrParser; + break; + case MessageLevel::StdOut: + parser = &m_stdoutParser; + break; + default: + return false; + } + + parser->appendLine(line); + auto items = parser->parseAvailable(); + if (auto err = parser->getError(); err.has_value()) { + auto& model = *getLogModel(); + model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); + return false; + } + + if (items.isEmpty()) + return true; + + auto model = getLogModel(); + for (auto const& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + msg = censorPrivateInfo(msg); + model->append(entry.level, msg); + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + + MessageLevel newLevel = MessageLevel::takeFromLine(msg); + + if (newLevel == MessageLevel::Unknown) + newLevel = LogParser::guessLevel(line, model->previousLevel()); + + msg = censorPrivateInfo(msg); + + model->append(newLevel, msg); + } } + + return true; } -void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) +void LaunchTask::onLogLines(const QStringList& lines, MessageLevel defaultLevel) { - // if the launcher part set a log level, use it - auto innerLevel = MessageLevel::fromLine(line); - if (innerLevel != MessageLevel::Unknown) { - level = innerLevel; + for (auto& line : lines) { + onLogLine(line, defaultLevel); } +} - // If the level is still undetermined, guess level - if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { - level = m_instance->guessLevel(line, level); +void LaunchTask::onLogLine(QString line, MessageLevel level) +{ + if (parseXmlLogs(line, level)) { + return; } // censor private user info line = censorPrivateInfo(line); - auto& model = *getLogModel(); - model.append(level, line); + getLogModel()->append(level, line); } void LaunchTask::emitSucceeded() diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index 2e87ece95..c52273a9e 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -39,29 +39,29 @@ #include #include #include -#include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" +#include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(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; } @@ -105,8 +105,8 @@ class LaunchTask : public Task { void requestLogging(); public slots: - void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); - void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onLogLines(const QStringList& lines, MessageLevel defaultLevel = MessageLevel::Launcher); + void onLogLine(QString line, MessageLevel defaultLevel = MessageLevel::Launcher); void onReadyForLaunch(); void onStepFinished(); void onProgressReportingRequested(); @@ -114,12 +114,17 @@ class LaunchTask : public Task { private: /*methods */ void finalizeSteps(bool successful, const QString& error); + protected: + 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; int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; + LogParser m_stdoutParser; + LogParser m_stderrParser; }; diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 23a33ae18..117867c1d 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -24,13 +24,13 @@ QVariant LogModel::data(const QModelIndex& index, int role) const return m_content[realRow].line; } if (role == LevelRole) { - return m_content[realRow].level; + return static_cast(m_content[realRow].level); } return QVariant(); } -void LogModel::append(MessageLevel::Enum level, QString line) +void LogModel::append(MessageLevel level, QString line) { if (m_suspended) { return; @@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines) return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary - QVector newContent; + QList newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over @@ -149,3 +149,28 @@ bool LogModel::wrapLines() const { return m_lineWrap; } + +void LogModel::setColorLines(bool state) +{ + if (m_colorLines != state) { + m_colorLines = state; + } +} + +bool LogModel::colorLines() const +{ + return m_colorLines; +} + +bool LogModel::isOverFlow() +{ + return m_numLines >= m_maxLines && m_stopOnOverflow; +} + +MessageLevel LogModel::previousLevel() +{ + if (m_numLines > 0) { + return m_content[m_numLines - 1].level; + } + return MessageLevel::Unknown; +} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 167f74190..847a41f5f 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -12,7 +12,7 @@ class LogModel : public QAbstractListModel { int rowCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; - void append(MessageLevel::Enum, QString line); + void append(MessageLevel, QString line); void clear(); void suspend(bool suspend); @@ -24,20 +24,25 @@ class LogModel : public QAbstractListModel { void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); + bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; + void setColorLines(bool state); + bool colorLines() const; + + MessageLevel previousLevel(); enum Roles { LevelRole = Qt::UserRole }; private /* types */: struct entry { - MessageLevel::Enum level = MessageLevel::Enum::Unknown; + MessageLevel level = MessageLevel::Unknown; QString line; }; private: /* data */ - QVector m_content; + QList m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; @@ -47,6 +52,7 @@ class LogModel : public QAbstractListModel { QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; + bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 946560c10..6b960974e 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -49,19 +49,15 @@ void PostLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); -#else - m_process.start(cmd); -#endif } void PostLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [&]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 3505febf7..7e843ca3f 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -49,18 +49,14 @@ void PreLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); -#else - m_process.start(cmd); -#endif } void PreLaunchCommand::on_state(LoggedProcess::State state) { - auto getError = [&]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; + auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: diff --git a/launcher/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp index 0dec35b79..53aff807e 100644 --- a/launcher/launch/steps/TextPrint.cpp +++ b/launcher/launch/steps/TextPrint.cpp @@ -1,11 +1,11 @@ #include "TextPrint.h" -TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level) : LaunchStep(parent) +TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level) : LaunchStep(parent) { m_lines = lines; m_level = level; } -TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level) : LaunchStep(parent) +TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel level) : LaunchStep(parent) { m_lines.append(line); m_level = level; diff --git a/launcher/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h index a96c2f887..4479a260a 100644 --- a/launcher/launch/steps/TextPrint.h +++ b/launcher/launch/steps/TextPrint.h @@ -26,8 +26,8 @@ class TextPrint : public LaunchStep { Q_OBJECT public: - explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel::Enum level); - explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel::Enum level); + explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level); + explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel level); virtual ~TextPrint() {}; virtual void executeTask(); @@ -36,5 +36,5 @@ class TextPrint : public LaunchStep { private: QStringList m_lines; - MessageLevel::Enum m_level; + MessageLevel m_level; }; diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp new file mode 100644 index 000000000..b808b35a3 --- /dev/null +++ b/launcher/logs/AnonymizeLog.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AnonymizeLog.h" + +#include + +struct RegReplace { + RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } + QRegularExpression reg; + QString with; +}; + +static const QVector anonymizeRules = { + RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), + "C:\\Users\\********\\"), // windows + RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), + "C:/Users/********/"), // windows with forward slashes + RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN + RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "new refresh token: \"\""), // refresh token + RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "\"device_code\" : \"\""), // device code +}; + +void anonymizeLog(QString& log) +{ + for (auto rule : anonymizeRules) { + log.replace(rule.reg, rule.with); + } +} diff --git a/launcher/Untar.h b/launcher/logs/AnonymizeLog.h similarity index 78% rename from launcher/Untar.h rename to launcher/logs/AnonymizeLog.h index 50e3a16e3..215d1e468 100644 --- a/launcher/Untar.h +++ b/launcher/logs/AnonymizeLog.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023-2024 Trial97 + * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ * 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 * @@ -33,14 +34,7 @@ * limitations under the License. */ #pragma once -#include -// this is a hack used for the java downloader (feel free to remove it in favor of a library) -// both extract functions will extract the first folder inside dest(disregarding the prefix) -namespace Tar { -bool extract(QIODevice* in, QString dst); -} +#include -namespace GZTar { -bool extract(QString src, QString dst); -} \ No newline at end of file +void anonymizeLog(QString& log); diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp new file mode 100644 index 000000000..bab4b9b9f --- /dev/null +++ b/launcher/logs/LogParser.cpp @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "LogParser.h" + +#include +#include "MessageLevel.h" + +using namespace Qt::Literals::StringLiterals; + +void LogParser::appendLine(QAnyStringView data) +{ + if (!m_partialData.isEmpty()) { + m_buffer = QString(m_partialData); + m_buffer.append("\n"); + m_partialData.clear(); + } + m_buffer.append(data.toString()); +} + +std::optional LogParser::getError() +{ + return m_error; +} + +std::optional LogParser::parseAttributes() +{ + LogParser::LogEntry entry{ + "", + MessageLevel::Info, + }; + auto attributes = m_parser.attributes(); + + for (const auto& attr : attributes) { + auto name = attr.name(); + auto value = attr.value(); + if (name == "logger"_L1) { + entry.logger = value.trimmed().toString(); + } else if (name == "timestamp"_L1) { + if (value.trimmed().isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); + return {}; + } + entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); + } else if (name == "level"_L1) { + entry.levelText = value.trimmed().toString(); + entry.level = MessageLevel::fromName(entry.levelText); + } else if (name == "thread"_L1) { + entry.thread = value.trimmed().toString(); + } + } + if (entry.logger.isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: logger"); + return {}; + } + + return entry; +} + +void LogParser::setError() +{ + m_error = { + m_parser.errorString(), + m_parser.error(), + }; +} + +void LogParser::clearError() +{ + m_error = {}; // clear previous error +} + +bool isPotentialLog4JStart(QStringView buffer) +{ + static QString target = QStringLiteral(" LogParser::parseNext() +{ + clearError(); + + if (m_buffer.isEmpty()) { + return {}; + } + + if (m_buffer.trimmed().isEmpty()) { + auto text = QString(m_buffer); + m_buffer.clear(); + return LogParser::PlainText{ text }; + } + + // check if we have a full xml log4j event + bool isCompleteLog4j = false; + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + if (m_parser.readNextStartElement()) { + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + int depth = 1; + bool eod = false; + while (depth > 0 && !eod) { + auto tok = m_parser.readNext(); + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + depth += 1; + } break; + case QXmlStreamReader::TokenType::EndElement: { + depth -= 1; + } break; + case QXmlStreamReader::TokenType::EndDocument: { + eod = true; // break outer while loop + } break; + default: { + // no op + } + } + if (m_parser.hasError()) { + break; + } + } + + isCompleteLog4j = depth == 0; + } + } + + if (isCompleteLog4j) { + return parseLog4J(); + } else { + if (isPotentialLog4JStart(m_buffer)) { + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + + int start = 0; + auto bufView = QStringView(m_buffer); + while (start < bufView.length()) { + if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { + auto slicestart = start + pos; + auto slice = bufView.right(bufView.length() - slicestart); + if (isPotentialLog4JStart(slice)) { + if (slicestart > 0) { + auto text = m_buffer.left(slicestart); + m_buffer = m_buffer.right(m_buffer.length() - slicestart); + if (!text.trimmed().isEmpty()) { + return LogParser::PlainText{ text }; + } + } + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + start = slicestart + 1; + } else { + break; + } + } + + // no log4j found, all plain text + auto text = QString(m_buffer); + m_buffer.clear(); + return LogParser::PlainText{ text }; + } +} + +QList LogParser::parseAvailable() +{ + QList items; + bool doNext = true; + while (doNext) { + auto item_ = parseNext(); + if (m_error.has_value()) { + return {}; + } + if (item_.has_value()) { + auto item = item_.value(); + if (std::holds_alternative(item)) { + break; + } else { + items.push_back(item); + } + } else { + doNext = false; + } + } + return items; +} + +std::optional LogParser::parseLog4J() +{ + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + + m_parser.readNextStartElement(); + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + auto entry_ = parseAttributes(); + if (!entry_.has_value()) { + setError(); + return {}; + } + auto entry = entry_.value(); + + bool foundMessage = false; + int depth = 1; + + enum parseOp { noOp, entryReady, parseError }; + + auto foundStart = [&]() -> parseOp { + depth += 1; + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + QString message; + bool messageComplete = false; + + while (!messageComplete) { + auto tok = m_parser.readNext(); + + switch (tok) { + case QXmlStreamReader::TokenType::Characters: { + message.append(m_parser.text()); + } break; + case QXmlStreamReader::TokenType::EndElement: { + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + messageComplete = true; + } + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return parseError; // parse fail + } break; + default: { + // no op + } + } + + if (m_parser.hasError()) { + return parseError; + } + } + + entry.message = message; + foundMessage = true; + depth -= 1; + } + return noOp; + }; + + auto foundEnd = [&]() -> parseOp { + depth -= 1; + if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + if (foundMessage) { + auto consumed = m_parser.characterOffset(); + if (consumed > 0 && consumed <= m_buffer.length()) { + m_buffer = m_buffer.right(m_buffer.length() - consumed); + // potential whitespace preserved for next item + } + clearError(); + return entryReady; + } + m_parser.raiseError("log4j:Event Missing required attribute: message"); + setError(); + return parseError; + } + return noOp; + }; + + while (!m_parser.atEnd()) { + auto tok = m_parser.readNext(); + parseOp op = noOp; + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + op = foundStart(); + } break; + case QXmlStreamReader::TokenType::EndElement: { + op = foundEnd(); + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return {}; + } break; + default: { + // no op + } + } + + switch (op) { + case parseError: + return {}; // parse fail or error + case entryReady: + return entry; + case noOp: + default: { + // no op + } + } + + if (m_parser.hasError()) { + return {}; + } + } + } + + throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); +} + +MessageLevel LogParser::guessLevel(const QString& line, MessageLevel previous) +{ + static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = LINE_WITH_LEVEL.match(line); + if (match.hasMatch()) { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + + return MessageLevel::fromName(levelStr); + } else { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || + line.contains("[FINEST]")) + return MessageLevel::Info; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + return MessageLevel::Error; + if (line.contains("[WARNING]")) + return MessageLevel::Warning; + if (line.contains("[DEBUG]")) + return MessageLevel::Debug; + } + + if (line.contains("Exception: ") || line.contains("Throwable: ")) + return MessageLevel::Error; + + if (line.startsWith("Caused by: ") || line.startsWith("Exception in thread")) + return MessageLevel::Error; + + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + + if (line.startsWith("\t") || line.startsWith(" ")) + return previous; + + return MessageLevel::Unknown; +} diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h new file mode 100644 index 000000000..ae657297c --- /dev/null +++ b/launcher/logs/LogParser.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include "MessageLevel.h" + +class LogParser { + public: + struct LogEntry { + QString logger; + MessageLevel level; + QString levelText; + QDateTime timestamp; + QString thread; + QString message; + }; + struct Partial { + QString data; + }; + struct PlainText { + QString message; + }; + struct Error { + QString errMessage; + QXmlStreamReader::Error error; + }; + + using ParsedItem = std::variant; + + public: + LogParser() = default; + + void appendLine(QAnyStringView data); + std::optional parseNext(); + QList parseAvailable(); + std::optional getError(); + + /// guess log level from a line of game log + static MessageLevel guessLevel(const QString& line, MessageLevel previous); + + protected: + std::optional parseAttributes(); + void setError(); + void clearError(); + + std::optional parseLog4J(); + + private: + QString m_buffer; + QString m_partialData; + QXmlStreamReader m_parser; + std::optional m_error; +}; diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.h b/launcher/macsandbox/SecurityBookmarkFileAccess.h new file mode 100644 index 000000000..69b344af9 --- /dev/null +++ b/launcher/macsandbox/SecurityBookmarkFileAccess.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FILEACCESS_H +#define FILEACCESS_H + +#include +#include +Q_FORWARD_DECLARE_OBJC_CLASS(NSData); +Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); +Q_FORWARD_DECLARE_OBJC_CLASS(NSString); +Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool); +Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary); +Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet); +class QString; +class QByteArray; +class QUrl; + +class SecurityBookmarkFileAccess { + /// The keys are bookmarks and the values are URLs. + NSMutableDictionary* m_bookmarks; + /// The keys are paths and the values are bookmarks. + NSMutableDictionary* m_paths; + /// Contains URLs that are currently being accessed. + NSMutableSet* m_activeURLs; + + bool m_readOnly; + + NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale); + + public: + /// \param readOnly A boolean indicating whether the bookmark should be read-only. + SecurityBookmarkFileAccess(bool readOnly = false); + ~SecurityBookmarkFileAccess(); + + /// Get a security scoped bookmark from a URL. + /// + /// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling + /// this function. Note that this is called implicitly if the user selects the directory from a file picker. + /// \param url The URL to get the security scoped bookmark from. + /// \return A QByteArray containing the security scoped bookmark. + QByteArray urlToSecurityScopedBookmark(const QUrl& url); + /// Get a security scoped bookmark from a path. + /// + /// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling + /// this function. Note that this is called implicitly if the user selects the directory from a file picker. + /// \param path The path to get the security scoped bookmark from. + /// \return A QByteArray containing the security scoped bookmark. + QByteArray pathToSecurityScopedBookmark(const QString& path); + /// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated. + /// + /// You must check whether the URL is valid before using it. + /// \param bookmark The security scoped bookmark to get the URL from. + /// \param isStale A boolean that will be set to true if the bookmark is stale. + /// \return The URL from the security scoped bookmark. + QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale); + + /// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this + /// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has + /// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that + /// this is the only `SecurityBookmarkFileAccess` accessing the resource. + /// + /// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be + /// updated. + /// \param bookmark The security scoped bookmark to start accessing. + /// \param isStale A boolean that will be set to true if the bookmark is stale. + /// \return A boolean indicating whether the bookmark was successfully accessed. + bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale); + void stopUsingSecurityScopedBookmark(QByteArray& bookmark); + + /// Returns true if access to the `path` is currently being maintained by this object. + bool isAccessingPath(const QString& path); +}; + +#endif // FILEACCESS_H diff --git a/launcher/macsandbox/SecurityBookmarkFileAccess.mm b/launcher/macsandbox/SecurityBookmarkFileAccess.mm new file mode 100644 index 000000000..bee854abe --- /dev/null +++ b/launcher/macsandbox/SecurityBookmarkFileAccess.mm @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SecurityBookmarkFileAccess.h" + +#include +#include +#include + +QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url) +{ + if (!url.isLocalFile()) + return {}; + + NSError* error = nil; + NSURL* nsurl = [url.toNSURL() absoluteURL]; + NSData* bookmark; + if ([m_paths objectForKey:[nsurl path]]) { + bookmark = m_paths[[nsurl path]]; + } else { + bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | + (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) + includingResourceValuesForKeys:nil + relativeToURL:nil + error:&error]; + } + if (error) { + return {}; + } + + // remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks + // sometimes you need to call this twice to actually stop access (extra calls aren't harmful) + [nsurl stopAccessingSecurityScopedResource]; + [nsurl stopAccessingSecurityScopedResource]; + nsurl = [NSURL URLByResolvingBookmarkData:bookmark + options:NSURLBookmarkResolutionWithSecurityScope | + (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) + relativeToURL:nil + bookmarkDataIsStale:nil + error:&error]; + m_paths[[nsurl path]] = bookmark; + m_bookmarks[bookmark] = nsurl; + + QByteArray qBookmark = QByteArray::fromNSData(bookmark); + bool isStale = false; + startUsingSecurityScopedBookmark(qBookmark, isStale); + + return qBookmark; +} + +SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly) +{ + m_bookmarks = [NSMutableDictionary new]; + m_paths = [NSMutableDictionary new]; + m_activeURLs = [NSMutableSet new]; +} + +SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess() +{ + for (NSURL* url : m_activeURLs) { + [url stopAccessingSecurityScopedResource]; + } +} + +QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path) +{ + return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path)); +} + +NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale) +{ + NSError* error = nil; + BOOL localStale = NO; + NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() + options:NSURLBookmarkResolutionWithSecurityScope | + (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) + relativeToURL:nil + bookmarkDataIsStale:&localStale + error:&error]; + if (error) { + return nil; + } + isStale = localStale; + if (isStale) { + NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | + (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) + includingResourceValuesForKeys:nil + relativeToURL:nil + error:&error]; + if (error) { + return nil; + } + bookmark = QByteArray::fromNSData(nsBookmark); + } + + NSData* nsBookmark = bookmark.toNSData(); + m_paths[[nsurl path]] = nsBookmark; + m_bookmarks[nsBookmark] = nsurl; + + return nsurl; +} + +QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale) +{ + if (bookmark.isEmpty()) + return {}; + + NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale); + if (!url) + return {}; + + return QUrl::fromNSURL(url); +} + +bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale) +{ + NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] + : securityScopedBookmarkToNSURL(bookmark, isStale); + if ([m_activeURLs containsObject:url]) + return false; + + [url stopAccessingSecurityScopedResource]; + if ([url startAccessingSecurityScopedResource]) { + [m_activeURLs addObject:url]; + return true; + } + return false; +} + +void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark) +{ + if (![m_bookmarks objectForKey:bookmark.toNSData()]) + return; + NSURL* url = m_bookmarks[bookmark.toNSData()]; + + if ([m_activeURLs containsObject:url]) { + [url stopAccessingSecurityScopedResource]; + [url stopAccessingSecurityScopedResource]; + + [m_activeURLs removeObject:url]; + [m_paths removeObjectForKey:[url path]]; + [m_bookmarks removeObjectForKey:bookmark.toNSData()]; + } +} + +bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path) +{ + NSData* bookmark = [m_paths objectForKey:path.toNSString()]; + if (!bookmark && path.endsWith('/')) { + bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()]; + } + if (!bookmark) { + return false; + } + NSURL* url = [m_bookmarks objectForKey:bookmark]; + return [m_activeURLs containsObject:url]; +} diff --git a/launcher/main.cpp b/launcher/main.cpp index 611873c81..374414763 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -35,34 +35,8 @@ #include "Application.h" -// #define BREAK_INFINITE_LOOP -// #define BREAK_EXCEPTION -// #define BREAK_RETURN - -#ifdef BREAK_INFINITE_LOOP -#include -#include -#endif - int main(int argc, char* argv[]) { -#ifdef BREAK_INFINITE_LOOP - while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(250)); - } -#endif -#ifdef BREAK_EXCEPTION - throw 42; -#endif -#ifdef BREAK_RETURN - return 42; -#endif - -#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#endif - // initialize Qt Application app(argc, argv); @@ -84,6 +58,8 @@ int main(int argc, char* argv[]) Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); + + Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 1707854be..d0c7075cd 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -23,7 +23,7 @@ namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} -Index::Index(const QVector& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) +Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); @@ -103,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QVector lists = other->m_lists; + const QList lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); @@ -154,7 +154,7 @@ Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) { QEventLoop ev; auto task = loadVersion(uid, version); - QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); return get(uid, version); diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index 026a00c07..fe5bf2170 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); - explicit Index(const QVector& lists, QObject* parent = nullptr); + explicit Index(const QList& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -46,7 +46,7 @@ class Index : public QAbstractListModel, public BaseEntity { Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; - QVector lists() const { return m_lists; } + QList lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); @@ -60,7 +60,7 @@ class Index : public QAbstractListModel, public BaseEntity { void parse(const QJsonObject& obj) override; private: - QVector m_lists; + QList m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 86af7277e..db1947655 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -35,13 +35,13 @@ MetadataVersion currentFormatVersion() // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { - const QVector objects = requireIsArrayOf(obj, "packages"); - QVector lists; + const QList objects = requireIsArrayOf(obj, "packages"); + QList lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); - list->setName(ensureString(obj, "name", QString())); - list->setSha256(ensureString(obj, "sha256", QString())); + list->setName(obj["name"].toString()); + list->setSha256(obj["sha256"].toString()); return list; }); return std::make_shared(lists); @@ -52,14 +52,14 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob { Version::Ptr version = std::make_shared(uid, requireString(obj, "version")); version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); - version->setType(ensureString(obj, "type", QString())); - version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); - version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); + version->setType(obj["type"].toString()); + version->setRecommended(obj["recommended"].toBool()); + version->setVolatile(obj["volatile"].toBool()); RequireSet reqs, conflicts; parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); - if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) { + if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) { version->setSha256(sha256); } return version; @@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); - const QVector versionsRaw = requireIsArrayOf(obj, "versions"); - QVector versions; + const QList versionsRaw = requireIsArrayOf(obj, "versions"); + QList versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); @@ -89,7 +89,7 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) }); VersionList::Ptr list = std::make_shared(uid); - list->setName(ensureString(obj, "name", QString())); + list->setName(obj["name"].toString()); list->setVersions(versions); return list; } @@ -171,8 +171,8 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName) while (iter != reqArray.end()) { auto reqObject = requireObject(*iter); auto uid = requireString(reqObject, "uid"); - auto equals = ensureString(reqObject, "equals", QString()); - auto suggests = ensureString(reqObject, "suggests", QString()); + auto equals = reqObject["equals"].toString(); + auto suggests = reqObject["suggests"].toString(); ptr->insert({ uid, equals, suggests }); iter++; } diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index 74e71e91c..ce9a9cc8a 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -21,11 +21,11 @@ Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} -QString Meta::Version::descriptor() +QString Meta::Version::descriptor() const { return m_version; } -QString Meta::Version::name() +QString Meta::Version::name() const { if (m_data) return m_data->name; @@ -88,7 +88,7 @@ QString Meta::Version::localFilename() const ::Version Meta::Version::toComparableVersion() const { - return { const_cast(this)->descriptor() }; + return { descriptor() }; } void Meta::Version::setType(const QString& type) diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 46dc740da..a2bbc6176 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -19,8 +19,8 @@ #include "BaseVersion.h" #include +#include #include -#include #include #include "minecraft/VersionFile.h" @@ -40,8 +40,8 @@ class Version : public QObject, public BaseVersion, public BaseEntity { explicit Version(const QString& uid, const QString& version); virtual ~Version() = default; - QString descriptor() override; - QString name() override; + QString descriptor() const override; + QString name() const override; QString typeString() const override; QString uid() const { return m_uid; } @@ -60,7 +60,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { QString localFilename() const override; - [[nodiscard]] ::Version toComparableVersion() const; + ::Version toComparableVersion() const; public: // for usage by format parsers only void setType(const QString& type); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index f96355658..dfca52d87 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -158,8 +158,8 @@ Version::Ptr VersionList::getVersion(const QString& version) bool VersionList::hasVersion(QString version) const { - auto ver = - std::find_if(m_versions.constBegin(), m_versions.constEnd(), [&](Meta::Version::Ptr const& a) { return a->version() == version; }); + auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), + [version](Meta::Version::Ptr const& a) { return a->version() == version; }); return (ver != m_versions.constEnd()); } @@ -169,7 +169,7 @@ void VersionList::setName(const QString& name) emit nameChanged(name); } -void VersionList::setVersions(const QVector& versions) +void VersionList::setVersions(const QList& versions) { beginResetModel(); m_versions = versions; @@ -265,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); + [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); @@ -282,7 +282,7 @@ void VersionList::waitToLoad() return; QEventLoop ev; auto task = getLoadTask(); - QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); } diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 4215439db..18681b8ed 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; - [[nodiscard]] Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; @@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity { Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; - QVector versions() const { return m_versions; } + QList versions() const { return m_versions; } // this blocks until the version list is loaded void waitToLoad(); public: // for usage only by parsers void setName(const QString& name); - void setVersions(const QVector& versions); + void setVersions(const QList& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; @@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity { void updateListData(QList) override {} private: - QVector m_versions; + QList m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 4406d9b34..410d1e689 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -159,7 +159,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd if (key == "hash") { object.hash = value.toString(); } else if (key == "size") { - object.size = value.toDouble(); + object.size = value.toLongLong(); } } @@ -298,7 +298,8 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - return BuildConfig.RESOURCE_BASE + getRelPath(); + auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + return resourceURL + getRelPath(); } QString AssetObject::getRelPath() diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index ad7ef545c..5f114e942 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -462,7 +462,7 @@ void Component::waitLoadMeta() } } -void Component::setUpdateAction(UpdateAction action) +void Component::setUpdateAction(const UpdateAction& action) { m_updateAction = action; } diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index 203cc2241..eafdb8ed7 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -106,7 +106,7 @@ class Component : public QObject, public ProblemProvider { void waitLoadMeta(); - void setUpdateAction(UpdateAction action); + void setUpdateAction(const UpdateAction& action); void clearUpdateAction(); UpdateAction getUpdateAction(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 36a07ee72..56db20205 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -570,7 +570,7 @@ void ComponentUpdateTask::performUpdateActions() component->setVersion(cv.targetVersion); component->waitLoadMeta(); }, - [&component, &instance](const UpdateActionLatestRecommendedCompatible lrc) { + [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() @@ -748,7 +748,6 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; - taskSlot.error = msg; d->remoteTasksInProgress--; checkIfAllFinished(); } @@ -769,7 +768,9 @@ void ComponentUpdateTask::checkIfAllFinished() QStringList allErrorsList; for (auto& item : d->remoteLoadStatusList) { if (!item.succeeded) { - allErrorsList.append(item.error); + const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); + allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.") + .arg(component->getName(), component->m_version)); } } auto allErrors = allErrorsList.join("\n"); diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index 2fc0b6d9a..8ffb9c71e 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -15,7 +15,6 @@ struct RemoteLoadStatus { size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; - QString error; Task::Ptr task; }; diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 22db7d641..a2588064f 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -54,11 +54,11 @@ struct GradleSpecifier { 4 "jdk15" 5 "jar" */ - QRegularExpression matcher( + static const QRegularExpression s_matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); - QRegularExpressionMatch match = matcher.match(value); + QRegularExpressionMatch match = s_matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 4f04f0eb9..026f9c281 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -65,7 +65,7 @@ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, { bool local = isLocal(); // Lambda function to get the absolute file path - auto actualPath = [&](QString relPath) { + auto actualPath = [this, local, overridePath](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { @@ -115,7 +115,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC bool local = isLocal(); // Lambda function to check if a local file exists - auto check_local_file = [&](QString storage) { + auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); @@ -128,7 +128,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC }; // Lambda function to add a download request - auto add_download = [&](QString storage, QString url, QString sha1) { + auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); } @@ -198,7 +198,7 @@ QList Library::getDownloads(const RuntimeContext& runtimeC } } } else { - auto raw_dl = [&]() { + auto raw_dl = [this, raw_storage]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } @@ -242,13 +242,13 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const if (m_rules.empty()) { result = true; } else { - RuleAction ruleResult = Disallow; + Rule::Action ruleResult = Rule::Disallow; for (auto rule : m_rules) { - RuleAction temp = rule->apply(this, runtimeContext); - if (temp != Defer) + Rule::Action temp = rule.apply(runtimeContext); + if (temp != Rule::Defer) ruleResult = temp; } - result = result && (ruleResult == Allow); + result = result && (ruleResult == Rule::Allow); } if (isNative()) { result = result && !getCompatibleNative(runtimeContext).isNull(); diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index d3019e814..d827554aa 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -129,7 +129,7 @@ class Library { void setHint(const QString& hint) { m_hint = hint; } /// Set the load rules - void setRules(QList> rules) { m_rules = rules; } + void setRules(QList rules) { m_rules = rules; } /// Returns true if the library should be loaded (or extracted, in case of natives) bool isActive(const RuntimeContext& runtimeContext) const; @@ -203,7 +203,7 @@ class Library { bool applyRules = false; /// rules associated with the library - QList> m_rules; + QList m_rules; /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; diff --git a/launcher/minecraft/Logging.cpp b/launcher/minecraft/Logging.cpp index 92596de3e..8b6304205 100644 --- a/launcher/minecraft/Logging.cpp +++ b/launcher/minecraft/Logging.cpp @@ -19,7 +19,6 @@ */ #include "minecraft/Logging.h" -#include Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 9500ac002..2a6e64626 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -38,6 +38,7 @@ #include "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" +#include "Json.h" #include "QObjectPtr.h" #include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/CreateGameFolders.h" @@ -52,8 +53,6 @@ #include "FileSystem.h" #include "MMCTime.h" #include "java/JavaVersion.h" -#include "pathmatcher/MultiMatcher.h" -#include "pathmatcher/RegexpMatcher.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" @@ -85,19 +84,62 @@ #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" -#include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" #include "tools/BaseProfiler.h" #include +#include +#include +#include +#include #ifdef Q_OS_LINUX #include "MangoHud.h" #endif +#ifdef WITH_QTDBUS +#include +#endif + #define IBUS "@im=ibus" +[[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) +{ +#ifdef WITH_QTDBUS + if (!QDBusConnection::systemBus().isConnected()) + return false; + + QDBusInterface switcheroo("net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", "org.freedesktop.DBus.Properties", + QDBusConnection::systemBus()); + + if (!switcheroo.isValid()) + return false; + + QDBusReply reply = + switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); + if (!reply.isValid()) + return false; + + QDBusArgument arg = qvariant_cast(reply.value().variant()); + QList gpus; + arg >> gpus; + + for (const auto& gpu : gpus) { + QString name = qvariant_cast(gpu[QStringLiteral("Name")]); + bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); + if (!defaultGpu) { + QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); + for (int i = 0; i + 1 < envList.size(); i += 2) { + env.insert(envList[i], envList[i + 1]); + } + return true; + } + } +#endif + return false; +} + // all of this because keeping things compatible with deprecated old settings // if either of the settings {a, b} is true, this also resolves to true class OrSetting : public Setting { @@ -118,12 +160,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 +250,17 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); + m_settings->registerSetting("ExportRecommendedRAM"); + + auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); + auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); + + connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("OverrideModDownloadLoaders", false); + m_settings->registerSetting("ModDownloadLoaders", "[]"); qDebug() << "Instance-type specific settings were loaded!"; @@ -216,7 +271,7 @@ void MinecraftInstance::loadSpecificSettings() void MinecraftInstance::updateRuntimeContext() { - m_runtimeContext.updateFromInstanceSettings(m_settings); + m_runtimeContext.updateFromInstanceSettings(m_settings.get()); m_components->invalidateLaunchProfile(); } @@ -225,9 +280,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 @@ -255,9 +310,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, false, false); }); + connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(this, false, true); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); @@ -349,6 +404,16 @@ QString MinecraftInstance::nilModsDir() const return FS::PathCombine(gameRoot(), "nilmods"); } +QString MinecraftInstance::dataPacksDir() +{ + QString relativePath = settings()->get("GlobalDataPacksPath").toString(); + + if (relativePath.isEmpty()) + relativePath = "datapacks"; + + return QDir(gameRoot()).filePath(relativePath); +} + QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); @@ -421,6 +486,28 @@ QStringList MinecraftInstance::getNativeJars() return nativeJars; } +static QString replaceTokensIn(const QString& text, const QMap& with) +{ + // TODO: does this still work?? + QString result; + static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + QStringList list; + QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); + int lastCapturedEnd = 0; + while (i.hasNext()) { + QRegularExpressionMatch match = i.next(); + result.append(text.mid(lastCapturedEnd, match.capturedStart())); + QString key = match.captured(1); + auto iter = with.find(key); + if (iter != with.end()) { + result.append(*iter); + } + lastCapturedEnd = match.capturedEnd(); + } + result.append(text.mid(lastCapturedEnd)); + return result; +} + QStringList MinecraftInstance::extraArguments() { auto list = BaseInstance::extraArguments(); @@ -433,7 +520,11 @@ QStringList MinecraftInstance::extraArguments() } auto addn = m_components->getProfile()->getAddnJvmArguments(); if (!addn.isEmpty()) { - list.append(addn); + QMap tokenMapping = makeProfileVarMapping(m_components->getProfile()); + + for (const QString& item : addn) { + list.append(replaceTokensIn(item, tokenMapping)); + } } auto agents = m_components->getProfile()->getAgents(); for (auto agent : agents) { @@ -499,6 +590,16 @@ QStringList MinecraftInstance::javaArguments() "minecraft.exe.heapdump"); #endif + // LWJGL2 reads `LWJGL_DISABLE_XRANDR` to force disable xrandr usage and fall back to xf86videomode. + // It *SHOULD* check for the executable to exist before trying to use it for queries but it doesnt, + // so WE can and force disable xrandr if it is not available. +#ifdef Q_OS_LINUX + // LWJGL2 is "org.lwjgl" LWJGL3 is "org.lwjgl3" + if (m_components->getComponent("org.lwjgl") != nullptr && QStandardPaths::findExecutable("xrandr").isEmpty()) { + args << QString("-DLWJGL_DISABLE_XRANDR=true"); + } +#endif + int min = settings()->get("MinMemAlloc").toInt(); int max = settings()->get("MaxMemAlloc").toInt(); if (min < max) { @@ -530,7 +631,7 @@ QStringList MinecraftInstance::javaArguments() QString MinecraftInstance::getLauncher() { // use legacy launcher if the traits are set - if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch")) + if (isLegacy()) return "legacy"; return "standard"; @@ -551,6 +652,13 @@ QMap MinecraftInstance::getVariables() out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); +#ifdef Q_OS_MACOS + // get library for Steam overlay support + QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); + if (!steamDyldInsertLibraries.isEmpty()) { + out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); + } +#endif return out; } @@ -566,7 +674,8 @@ QProcessEnvironment MinecraftInstance::createEnvironment() } // custom env - auto insertEnv = [&env](QMap envMap) { + auto insertEnv = [&env](QString value) { + auto envMap = Json::toMap(value); if (envMap.isEmpty()) return; @@ -577,9 +686,9 @@ QProcessEnvironment MinecraftInstance::createEnvironment() bool overrideEnv = settings()->get("OverrideEnv").toBool(); if (!overrideEnv) - insertEnv(APPLICATION->settings()->get("Env").toMap()); + insertEnv(APPLICATION->settings()->get("Env").toString()); else - insertEnv(settings()->get("Env").toMap()); + insertEnv(settings()->get("Env").toString()); return env; } @@ -615,12 +724,14 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() } if (settings()->get("UseDiscreteGpu").toBool()) { - // Open Source Drivers - env.insert("DRI_PRIME", "1"); - // Proprietary Nvidia Drivers - env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); - env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); - env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + if (!switcherooSetupGPU(env)) { + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + } } if (settings()->get("UseZink").toBool()) { @@ -633,28 +744,6 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() return env; } -static QString replaceTokensIn(QString text, QMap with) -{ - // TODO: does this still work?? - QString result; - QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); - QStringList list; - QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); - int lastCapturedEnd = 0; - while (i.hasNext()) { - QRegularExpressionMatch match = i.next(); - result.append(text.mid(lastCapturedEnd, match.capturedStart())); - QString key = match.captured(1); - auto iter = with.find(key); - if (iter != with.end()) { - result.append(*iter); - } - lastCapturedEnd = match.capturedEnd(); - } - result.append(text.mid(lastCapturedEnd)); - return result; -} - QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); @@ -676,42 +765,26 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine } } - QMap token_mapping; + QMap tokenMapping = makeProfileVarMapping(profile); + // yggdrasil! if (session) { // token_mapping["auth_username"] = session->username; - token_mapping["auth_session"] = session->session; - token_mapping["auth_access_token"] = session->access_token; - token_mapping["auth_player_name"] = session->player_name; - token_mapping["auth_uuid"] = session->uuid; - token_mapping["user_properties"] = session->serializeUserProperties(); - token_mapping["user_type"] = session->user_type; + tokenMapping["auth_session"] = session->session; + tokenMapping["auth_access_token"] = session->access_token; + tokenMapping["auth_player_name"] = session->player_name; + tokenMapping["auth_uuid"] = session->uuid; + tokenMapping["user_properties"] = session->serializeUserProperties(); + tokenMapping["user_type"] = session->user_type; + if (session->demo) { args_pattern += " --demo"; } } - token_mapping["profile_name"] = name(); - token_mapping["version_name"] = profile->getMinecraftVersion(); - token_mapping["version_type"] = profile->getMinecraftVersionType(); - - QString absRootDir = QDir(gameRoot()).absolutePath(); - token_mapping["game_directory"] = absRootDir; - QString absAssetsDir = QDir("assets/").absolutePath(); - auto assets = profile->getMinecraftAssets(); - token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); - - // 1.7.3+ assets tokens - token_mapping["assets_root"] = absAssetsDir; - token_mapping["assets_index_name"] = assets->id; - -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); -#else - QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); -#endif for (int i = 0; i < parts.length(); i++) { - parts[i] = replaceTokensIn(parts[i], token_mapping); + parts[i] = replaceTokensIn(parts[i], tokenMapping); } return parts; } @@ -753,11 +826,30 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT // window size, title and state, legacy { QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) - windowParams = "maximized"; - else + if (settings()->get("LaunchMaximized").toBool()) { + // FIXME doesn't support maximisation + if (!isLegacy()) { + auto screen = QGuiApplication::primaryScreen(); + auto screenGeometry = screen->availableSize(); + + // small hack to get the widow decorations + for (auto w : QApplication::topLevelWidgets()) { + auto mainWindow = qobject_cast(w); + if (mainWindow) { + auto m = mainWindow->windowHandle()->frameMargins(); + screenGeometry = screenGeometry.shrunkBy(m); + break; + } + } + + windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); + } else { + windowParams = "maximized"; + } + } else { windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); + } launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; } @@ -829,7 +921,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << "Libraries:"; QStringList jars, nativeJars; profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); - auto printLibFile = [&](const QString& path) { + auto printLibFile = [&out](const QString& path) { QFileInfo info(path); if (info.exists()) { out << " " + path; @@ -849,7 +941,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr } // mods and core mods - auto printModList = [&](const QString& label, ModFolderModel& model) { + auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); @@ -874,8 +966,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr } }; - printModList("Mods", *(loaderModList().get())); - printModList("Core Mods", *(coreModList().get())); + printModList("Mods", *loaderModList()); + printModList("Core Mods", *coreModList()); // jar mods auto& jarMods = profile->getJarMods(); @@ -937,61 +1029,32 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess return filter; } -MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) +QMap MinecraftInstance::makeProfileVarMapping(std::shared_ptr profile) const { - QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - auto match = re.match(line); - if (match.hasMatch()) { - // New style logs from log4j - QString timestamp = match.captured("timestamp"); - QString levelStr = match.captured("level"); - if (levelStr == "INFO") - level = MessageLevel::Message; - if (levelStr == "WARN") - level = MessageLevel::Warning; - if (levelStr == "ERROR") - level = MessageLevel::Error; - if (levelStr == "FATAL") - level = MessageLevel::Fatal; - if (levelStr == "TRACE" || levelStr == "DEBUG") - level = MessageLevel::Debug; - } else { - // Old style forge logs - if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || - line.contains("[FINEST]")) - level = MessageLevel::Message; - if (line.contains("[SEVERE]") || line.contains("[STDERR]")) - level = MessageLevel::Error; - if (line.contains("[WARNING]")) - level = MessageLevel::Warning; - if (line.contains("[DEBUG]")) - level = MessageLevel::Debug; - } - if (line.contains("overwriting existing")) - return MessageLevel::Fatal; - // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * - static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; - if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at " + javaSymbol)) || - line.contains(QRegularExpression("Caused by: " + javaSymbol)) || - line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) || - line.contains(QRegularExpression("... \\d+ more$"))) - return MessageLevel::Error; - return level; -} + QMap result; -IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() -{ - auto combined = std::make_shared(); - combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); - combined->add(std::make_shared("crash-.*\\.txt")); - combined->add(std::make_shared("IDMap dump.*\\.txt$")); - combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); - return combined; + result["profile_name"] = name(); + result["version_name"] = profile->getMinecraftVersion(); + result["version_type"] = profile->getMinecraftVersionType(); + + QString absRootDir = QDir(gameRoot()).absolutePath(); + result["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = profile->getMinecraftAssets(); + result["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); + + // 1.7.3+ assets tokens + result["assets_root"] = absAssetsDir; + result["assets_index_name"] = assets->id; + + result["library_directory"] = APPLICATION->metacache()->getBasePath("libraries"); + + return result; } -QString MinecraftInstance::getLogFileRoot() +QStringList MinecraftInstance::getLogFileSearchPaths() { - return gameRoot(); + return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() @@ -1045,11 +1108,10 @@ QList MinecraftInstance::createUpdateTask() }; } -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"); @@ -1164,9 +1226,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() @@ -1174,71 +1236,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)); + 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)); + 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) { - m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); } - return m_resource_pack_list; + return m_resource_pack_list.get(); } -std::shared_ptr MinecraftInstance::texturePackList() +TexturePackFolderModel* MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); } - return m_texture_pack_list; + return m_texture_pack_list.get(); } -std::shared_ptr MinecraftInstance::shaderPackList() +ShaderPackFolderModel* MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { - m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this)); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); } - return m_shader_pack_list; + return m_shader_pack_list.get(); } -std::shared_ptr MinecraftInstance::worldList() +DataPackFolderModel* MinecraftInstance::dataPackList() { - if (!m_world_list) { - m_world_list.reset(new WorldList(worldDir(), this)); + 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_world_list; + return m_data_pack_list.get(); +} + +QList MinecraftInstance::resourceLists() +{ + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } -std::shared_ptr MinecraftInstance::gameOptionsModel() +WorldList* MinecraftInstance::worldList() { - if (!m_game_options) { - m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); + if (!m_world_list) { + m_world_list.reset(new WorldList(worldDir(), this)); } - return m_game_options; + return m_world_list.get(); } QList MinecraftInstance::getJarMods() const diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 75e97ae45..909962d5e 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -36,6 +36,7 @@ #pragma once #include +#include #include #include #include "BaseInstance.h" @@ -48,15 +49,15 @@ class ResourcePackFolderModel; class ShaderPackFolderModel; class TexturePackFolderModel; class WorldList; -class GameOptions; class LaunchStep; +class LaunchProfile; class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: - MinecraftInstance(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; @@ -80,6 +81,7 @@ class MinecraftInstance : public BaseInstance { QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; + QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; @@ -102,26 +104,27 @@ class MinecraftInstance : public BaseInstance { QString getLocalLibraryPath() const; /** Returns whether the instance, with its version, has support for demo mode. */ - [[nodiscard]] bool supportsDemo() const; + bool supportsDemo() const; 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 worldList(); - std::shared_ptr gameOptionsModel(); + 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; @@ -138,12 +141,7 @@ class MinecraftInstance : public BaseInstance { QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; - /// guess log level from a line of minecraft log - MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override; - - IPathMatcher::Ptr getLogFileMatcher() override; - - QString getLogFileRoot() override; + QStringList getLogFileSearchPaths() override; QString getStatusbarDescription() override; @@ -161,17 +159,16 @@ class MinecraftInstance : public BaseInstance { protected: QMap createCensorFilterFromSession(AuthSessionPtr session); + 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_world_list; - mutable std::shared_ptr m_game_options; + std::unique_ptr m_components; + std::unique_ptr m_loader_mod_list; + std::unique_ptr m_core_mod_list; + std::unique_ptr m_nil_mod_list; + std::unique_ptr m_resource_pack_list; + std::unique_ptr m_shader_pack_list; + std::unique_ptr m_texture_pack_list; + std::unique_ptr m_data_pack_list; + std::unique_ptr m_world_list; }; - -using MinecraftInstancePtr = std::shared_ptr; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index c0a82e61e..149e7cf19 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -43,4 +43,4 @@ bool MinecraftLoadAndCheck::abort() return status; } return Task::abort(); -} \ No newline at end of file +} diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index d17a3a21f..42730b12d 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -319,7 +319,11 @@ LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, cons } if (libObj.contains("rules")) { out->applyRules = true; - out->m_rules = rulesFromJsonV4(libObj); + + QJsonArray rulesArray = requireArray(libObj.value("rules")); + for (auto rule : rulesArray) { + out->m_rules.append(Rule::fromJson(requireObject(rule))); + } } if (libObj.contains("downloads")) { out->m_mojangDownloads = libDownloadInfoFromJson(libObj); @@ -355,7 +359,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library* library) if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto& rule : library->m_rules) { - QJsonObject ruleObj = rule->toJson(); + QJsonObject ruleObj = rule.toJson(); allRules.append(ruleObj); } libRoot.insert("rules", allRules); diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index bd587beb2..48ea3b894 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->uid = root.value("fileId").toString(); } - const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern( + static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; - if (!valid_uid_regex.match(out->uid).hasMatch()) { + if (!s_validUidRegex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); @@ -176,7 +176,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc } } - auto readLibs = [&](const char* which, QList& outList) { + auto readLibs = [&root, &out, &filename](const char* which, QList& outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library @@ -259,8 +259,8 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc if (root.contains("runtimes")) { out->runtimes = {}; - for (auto runtime : ensureArray(root, "runtimes")) { - out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime))); + for (auto runtime : root["runtimes"].toArray()) { + out->runtimes.append(Java::parseJavaMeta(runtime.toObject())); } } diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 92242d452..4160351b4 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -38,7 +38,6 @@ */ #include -#include #include #include #include @@ -130,18 +129,18 @@ static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& comp auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); auto component = makeShared(parent, uid); - component->m_version = Json::ensureString(obj.value("version")); - component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); - component->m_important = Json::ensureBoolean(obj.value("important"), false); + component->m_version = obj.value("version").toString(); + component->m_dependencyOnly = obj.value("dependencyOnly").toBool(); + component->m_important = obj.value("important").toBool(); // cached // TODO @RESILIENCE: ignore invalid values/structure here? - component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); - component->m_cachedName = Json::ensureString(obj.value("cachedName")); + component->m_cachedVersion = obj.value("cachedVersion").toString(); + component->m_cachedName = obj.value("cachedName").toString(); Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); - component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); - bool disabled = Json::ensureBoolean(obj.value("disabled"), false); + component->m_cachedVolatile = obj.value("volatile").toBool(); + bool disabled = obj.value("disabled").toBool(); component->setEnabled(!disabled); return component; } @@ -517,13 +516,9 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const switch (role) { case Qt::CheckStateRole: { - switch (column) { - case NameColumn: { - return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; - } - default: - return QVariant(); - } + if (column == NameColumn) + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); } case Qt::DisplayRole: { switch (column) { @@ -649,11 +644,7 @@ void PackProfile::move(const int index, const MoveDirection direction) return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) d->components.swapItemsAt(index, theirIndex); -#else - d->components.swap(index, theirIndex); -#endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); @@ -752,7 +743,7 @@ bool PackProfile::removeComponent_internal(ComponentPtr patch) } // FIXME: we need a generic way of removing local resources, not just jar mods... - auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool { + auto preRemoveJarMod = [this](LibraryPtr jarMod) -> bool { if (!jarMod->isLocal()) { return true; } diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index 08ec0fac3..a79f89529 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -41,7 +41,6 @@ #include #include -#include #include namespace ProfileUtils { diff --git a/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp index d80aab84d..606776e8a 100644 --- a/launcher/minecraft/Rule.cpp +++ b/launcher/minecraft/Rule.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,72 +39,54 @@ #include "Rule.h" -RuleAction RuleAction_fromString(QString name) +Rule Rule::fromJson(const QJsonObject& object) { - if (name == "allow") - return Allow; - if (name == "disallow") - return Disallow; - return Defer; + Rule result; + + if (object["action"] == "allow") + result.m_action = Allow; + else if (object["action"] == "disallow") + result.m_action = Disallow; + + if (auto os = object["os"]; os.isObject()) { + if (auto name = os["name"].toString(); !name.isNull()) { + result.m_os = OS{ + name, + os["version"].toString(), + }; + } + } + + return result; } -QList> rulesFromJsonV4(const QJsonObject& objectWithRules) +QJsonObject Rule::toJson() { - QList> rules; - auto rulesVal = objectWithRules.value("rules"); - if (!rulesVal.isArray()) - return rules; + QJsonObject result; - QJsonArray ruleList = rulesVal.toArray(); - for (auto ruleVal : ruleList) { - std::shared_ptr rule; - if (!ruleVal.isObject()) - continue; - auto ruleObj = ruleVal.toObject(); - auto actionVal = ruleObj.value("action"); - if (!actionVal.isString()) - continue; - auto action = RuleAction_fromString(actionVal.toString()); - if (action == Defer) - continue; + if (m_action == Allow) + result["action"] = "allow"; + else if (m_action == Disallow) + result["action"] = "disallow"; - auto osVal = ruleObj.value("os"); - if (!osVal.isObject()) { - // add a new implicit action rule - rules.append(ImplicitRule::create(action)); - continue; - } + if (m_os.has_value()) { + QJsonObject os; + + os["name"] = m_os->name; - auto osObj = osVal.toObject(); - auto osNameVal = osObj.value("name"); - if (!osNameVal.isString()) - continue; - QString osName = osNameVal.toString(); - QString versionRegex = osObj.value("version").toString(); - // add a new OS rule - rules.append(OsRule::create(action, osName, versionRegex)); + if (!m_os->version.isEmpty()) + os["version"] = m_os->version; + + result["os"] = os; } - return rules; -} -QJsonObject ImplicitRule::toJson() -{ - QJsonObject ruleObj; - ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); - return ruleObj; + return result; } -QJsonObject OsRule::toJson() +Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { - QJsonObject ruleObj; - ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); - QJsonObject osObj; - { - osObj.insert("name", m_system); - if (!m_version_regexp.isEmpty()) { - osObj.insert("version", m_version_regexp); - } - } - ruleObj.insert("os", osObj); - return ruleObj; + if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) + return Defer; + + return m_action; } diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index c6cdbc43f..b0b689fd7 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,59 +39,27 @@ #include #include #include -#include #include "RuntimeContext.h" class Library; -class Rule; - -enum RuleAction { Allow, Disallow, Defer }; - -QList> rulesFromJsonV4(const QJsonObject& objectWithRules); class Rule { - protected: - RuleAction m_result; - virtual bool applies(const Library* parent, const RuntimeContext& runtimeContext) = 0; - public: - Rule(RuleAction result) : m_result(result) {} - virtual ~Rule() {} - virtual QJsonObject toJson() = 0; - RuleAction apply(const Library* parent, const RuntimeContext& runtimeContext) - { - if (applies(parent, runtimeContext)) - return m_result; - else - return Defer; - } -}; - -class OsRule : public Rule { - private: - // the OS - QString m_system; - // the OS version regexp - QString m_version_regexp; + enum Action { Allow, Disallow, Defer }; - protected: - virtual bool applies(const Library*, const RuntimeContext& runtimeContext) { return runtimeContext.classifierMatches(m_system); } - OsRule(RuleAction result, QString system, QString version_regexp) : Rule(result), m_system(system), m_version_regexp(version_regexp) {} + static Rule fromJson(const QJsonObject& json); + QJsonObject toJson(); - public: - virtual QJsonObject toJson(); - static std::shared_ptr create(RuleAction result, QString system, QString version_regexp) - { - return std::shared_ptr(new OsRule(result, system, version_regexp)); - } -}; + Action apply(const RuntimeContext& runtimeContext); -class ImplicitRule : public Rule { - protected: - virtual bool applies(const Library*, [[maybe_unused]] const RuntimeContext& runtimeContext) { return true; } - ImplicitRule(RuleAction result) : Rule(result) {} + private: + struct OS { + QString name; + // FIXME: unsupported + // retained to avoid information being lost from files + QString version; + }; - public: - virtual QJsonObject toJson(); - static std::shared_ptr create(RuleAction result) { return std::shared_ptr(new ImplicitRule(result)); } + Action m_action = Defer; + std::optional m_os; }; diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp new file mode 100644 index 000000000..ec4ebb31a --- /dev/null +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * parent program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * parent program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with parent program. If not, see . + * + * parent file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use parent file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ShortcutUtils.h" + +#include "FileSystem.h" + +#include +#include + +#include +#include +#include + +namespace ShortcutUtils { + +bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) +{ + if (!shortcut.instance) + return false; + + QString appPath = QApplication::applicationFilePath(); + auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } + QString iconPath; + QStringList args; +#if defined(Q_OS_MACOS) + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return false; + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + return false; + } + + QIcon iconObj = icon->icon(); + bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + return false; + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical( + shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); + } + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return false; + } + + if (DesktopServices::isFlatpak()) { + appPath = "flatpak"; + args.append({ "run", BuildConfig.LAUNCHER_APPID }); + } + +#elif defined(Q_OS_WIN) + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but parent 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->logo(); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return false; + } + +#else + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); + return false; +#endif + args.append({ "--launch", shortcut.instance->id() }); + args.append(shortcut.extraArgs); + + QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); + if (shortcutPath.isEmpty()) { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); + return false; + } + + shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); + return true; +} + +bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) +{ + if (!shortcut.instance) + return false; + + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); + return false; + } + + QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); + return true; +} + +bool createInstanceShortcutInApplications(const Shortcut& shortcut) +{ + if (!shortcut.instance) + return false; + + QString applicationsDir = FS::getApplicationsDir(); + if (applicationsDir.isEmpty()) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); + return false; + } + +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); + + QDir applicationsDirQ(applicationsDir); + if (!applicationsDirQ.mkpath(".")) { + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Failed to create instances folder in applications folder!")); + return false; + } +#endif + + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); + return true; +} + +bool createInstanceShortcutInOther(const Shortcut& shortcut) +{ + if (!shortcut.instance) + return false; + + QString defaultedDir = FS::getDesktopDir(); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString extension = ".desktop"; +#elif defined(Q_OS_WINDOWS) + QString extension = ".lnk"; +#else + QString extension = ""; +#endif + + QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); + + shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, + QObject::tr("Desktop Entries") + " (*" + extension + ")"); + if (shortcutFilePath.isEmpty()) + return false; // file dialog canceled by user + + if (shortcutFilePath.endsWith(extension)) + shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); + return true; +} + +} // namespace ShortcutUtils diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h new file mode 100644 index 000000000..b995c36bd --- /dev/null +++ b/launcher/minecraft/ShortcutUtils.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * 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 "Application.h" + +#include +#include + +namespace ShortcutUtils { +/// A struct to hold parameters for creating a shortcut +struct Shortcut { + BaseInstance* instance; + QString name; + QString targetString; + QWidget* parent = nullptr; + QStringList extraArgs = {}; + QString iconKey = ""; + ShortcutTarget target; +}; + +/// Create an instance shortcut on the specified file path +bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); + +/// Create an instance shortcut on the desktop +bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); + +/// Create an instance shortcut in the Applications directory +bool createInstanceShortcutInApplications(const Shortcut& shortcut); + +/// Create an instance shortcut in other directories +bool createInstanceShortcutInOther(const Shortcut& shortcut); + +} // namespace ShortcutUtils diff --git a/launcher/minecraft/VanillaInstanceCreationTask.cpp b/launcher/minecraft/VanillaInstanceCreationTask.cpp index ccbd8c677..420ffd3c3 100644 --- a/launcher/minecraft/VanillaInstanceCreationTask.cpp +++ b/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -19,20 +19,18 @@ bool 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()); - - inst.setName(name()); - inst.setIconKey(m_instIcon); - } - instance_settings->resumeSave(); + MinecraftInstance inst(m_globalSettings, std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), + m_stagingPath); + SettingsObject::Lock lock(inst.settings()); + + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if (m_using_loader) + components->setComponentVersion(m_loader, m_loader_version->descriptor()); + + inst.setName(name()); + inst.setIconKey(m_instIcon); return true; } diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index bd28f9e9a..bdbe721e3 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -43,9 +43,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -57,6 +54,7 @@ #include "FileSystem.h" #include "PSaveFile.h" +#include "archive/ArchiveReader.h" using std::nullopt; using std::optional; @@ -198,22 +196,6 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) return f.commit(); } -int64_t calculateWorldSize(const QFileInfo& file) -{ - if (file.isFile() && file.suffix() == "zip") { - return file.size(); - } else if (file.isDir()) { - QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); - int64_t total = 0; - while (it.hasNext()) { - it.next(); - total += it.fileInfo().size(); - } - return total; - } - return -1; -} - World::World(const QFileInfo& file) { repath(file); @@ -223,7 +205,6 @@ void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); - m_size = calculateWorldSize(file); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); @@ -252,45 +233,34 @@ void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { - is_valid = false; + m_isValid = false; return; } loadFromLevelDat(bytes); - levelDatTime = file.lastModified(); + m_levelDatTime = file.lastModified(); } void World::readFromZip(const QFileInfo& file) { - QuaZip zip(file.absoluteFilePath()); - is_valid = zip.open(QuaZip::mdUnzip); - if (!is_valid) { - return; - } - auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); - is_valid = !location.isEmpty(); - if (!is_valid) { - return; - } - m_containerOffsetPath = location; - QuaZipFile zippedFile(&zip); - // read the install profile - is_valid = zip.setCurrentFile(location + "level.dat"); - if (!is_valid) { - return; - } - is_valid = zippedFile.open(QIODevice::ReadOnly); - QuaZipFileInfo64 levelDatInfo; - zippedFile.getFileInfo(&levelDatInfo); - auto modTime = levelDatInfo.getNTFSmTime(); - if (!modTime.isValid()) { - modTime = levelDatInfo.dateTime; - } - levelDatTime = modTime; - if (!is_valid) { - return; - } - loadFromLevelDat(zippedFile.readAll()); - zippedFile.close(); + MMCZip::ArchiveReader r(file.absoluteFilePath()); + + m_isValid = false; + r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) { + const QString levelDat = "level.dat"; + auto filePath = file->filename(); + QFileInfo fi(filePath); + if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { + m_containerOffsetPath = filePath.chopped(levelDat.length()); + if (!m_containerOffsetPath.isEmpty()) { + return false; + } + m_levelDatTime = file->dateTime(); + loadFromLevelDat(file->readAll()); + m_isValid = true; + stop = true; + } + return true; + }); } bool World::install(const QString& to, const QString& name) @@ -301,10 +271,7 @@ bool World::install(const QString& to, const QString& name) } bool ok = false; if (m_containerFile.isFile()) { - QuaZip zip(m_containerFile.absoluteFilePath()); - if (!zip.open(QuaZip::mdUnzip)) { - return false; - } + MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath()); ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if (m_containerFile.isDir()) { QString from = m_containerFile.filePath(); @@ -367,7 +334,7 @@ optional read_string(nbt::value& parent, const char* name) return nullopt; } auto& tag_str = namedValue.as(); - return QString::fromStdString(tag_str.get()); + return QString::fromUtf8(tag_str.get()); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; @@ -430,7 +397,7 @@ void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { - is_valid = false; + m_isValid = false; return; } @@ -439,20 +406,20 @@ void World::loadFromLevelDat(QByteArray data) valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); - is_valid = false; + m_isValid = false; return; } nbt::value& val = *valPtr; - is_valid = val.get_type() == nbt::tag_type::Compound; - if (!is_valid) + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); - m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; m_gameType = read_gametype(val, "GameType"); @@ -490,7 +457,7 @@ bool World::replace(World& with) bool World::destroy() { - if (!is_valid) + if (!m_isValid) return false; if (FS::trash(m_containerFile.filePath())) @@ -508,7 +475,7 @@ bool World::destroy() bool World::operator==(const World& other) const { - return is_valid == other.is_valid && folderName() == other.folderName(); + return m_isValid == other.m_isValid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const @@ -531,3 +498,8 @@ bool World::isMoreThanOneHardLink() const } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } + +void World::setSize(int64_t size) +{ + m_size = size; +} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 4303dc553..bb4baa33d 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -39,7 +39,7 @@ class World { QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } - bool isValid() const { return is_valid; } + bool isValid() const { return m_isValid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world @@ -54,10 +54,12 @@ class World { bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); + void setSize(int64_t size); + // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; - [[nodiscard]] auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } + auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance @@ -66,9 +68,9 @@ class World { * @return true * @return false */ - [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + bool isSymLinkUnder(const QString& instPath) const; - [[nodiscard]] bool isMoreThanOneHardLink() const; + bool isMoreThanOneHardLink() const; QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } @@ -83,10 +85,10 @@ class World { QString m_folderName; QString m_actualName; QString m_iconFile; - QDateTime levelDatTime; + QDateTime m_levelDatTime; QDateTime m_lastPlayed; - int64_t m_size; + int64_t m_size = 0; int64_t m_randomSeed = 0; GameType m_gameType; - bool is_valid = false; + bool m_isValid = false; }; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 812b13c71..d075bf433 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -37,13 +37,14 @@ #include #include +#include #include #include #include +#include #include #include #include -#include "Application.h" WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { @@ -51,18 +52,18 @@ WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractList m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); - is_watching = false; + m_isWatching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -71,11 +72,11 @@ void WorldList::startWatching() void WorldList::stopWatching() { - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -101,12 +102,13 @@ bool WorldList::update() } } beginResetModel(); - worlds.swap(newWorlds); + m_worlds.swap(newWorlds); endResetModel(); + loadWorldsAsync(); return true; } -void WorldList::directoryChanged(QString path) +void WorldList::directoryChanged(QString) { update(); } @@ -123,12 +125,12 @@ QString WorldList::instDirPath() const bool WorldList::deleteWorld(int index) { - if (index >= worlds.size() || index < 0) + if (index >= m_worlds.size() || index < 0) return false; - World& m = worlds[index]; + World& m = m_worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); - worlds.removeAt(index); + m_worlds.removeAt(index); endRemoveRows(); emit changed(); return true; @@ -139,11 +141,11 @@ bool WorldList::deleteWorld(int index) bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { - World& m = worlds[i]; + World& m = m_worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); - worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; @@ -151,9 +153,9 @@ bool WorldList::deleteWorlds(int first, int last) bool WorldList::resetIcon(int row) { - if (row >= worlds.size() || row < 0) + if (row >= m_worlds.size() || row < 0) return false; - World& m = worlds[row]; + World& m = m_worlds[row]; if (m.resetIcon()) { emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); return true; @@ -174,12 +176,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= worlds.size()) + if (row < 0 || row >= m_worlds.size()) return QVariant(); QLocale locale; - auto& world = worlds[row]; + auto& world = m_worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { @@ -208,13 +210,9 @@ QVariant WorldList::data(const QModelIndex& index, int role) const } case Qt::UserRole: - switch (column) { - case SizeColumn: - return QVariant::fromValue(world.bytes()); - - default: - return data(index, Qt::DisplayRole); - } + if (column == SizeColumn) + return QVariant::fromValue(world.bytes()); + return data(index, Qt::DisplayRole); case Qt::ToolTipRole: { if (column == InfoColumn) { @@ -303,54 +301,31 @@ QStringList WorldList::mimeTypes() const return types; } -class WorldMimeData : public QMimeData { - Q_OBJECT - - public: - WorldMimeData(QList worlds) { m_worlds = worlds; } - QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } - - protected: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QVariant retrieveData(const QString& mimetype, QMetaType type) const -#else - QVariant retrieveData(const QString& mimetype, QVariant::Type type) const -#endif - { - QList urls; - for (auto& world : m_worlds) { - if (!world.isValid() || !world.isOnFS()) - continue; - QString worldPath = world.container().absoluteFilePath(); - qDebug() << worldPath; - urls.append(QUrl::fromLocalFile(worldPath)); - } - const_cast(this)->setUrls(urls); - return QMimeData::retrieveData(mimetype, type); - } - - private: - QList m_worlds; -}; - QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const { - if (indexes.size() == 0) - return new QMimeData(); + QList urls; - QList worlds_; for (auto idx : indexes) { if (idx.column() != 0) continue; + int row = idx.row(); - if (row < 0 || row >= this->worlds.size()) + if (row < 0 || row >= this->m_worlds.size()) continue; - worlds_.append(this->worlds[row]); - } - if (!worlds_.size()) { - return new QMimeData(); + + const World& world = m_worlds[row]; + + if (!world.isValid() || !world.isOnFS()) + continue; + + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); } - return new WorldMimeData(worlds_); + + auto result = new QMimeData(); + result->setUrls(urls); + return result; } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const @@ -397,7 +372,7 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; // files dropped from outside? if (data->hasUrls()) { - bool was_watching = is_watching; + bool was_watching = m_isWatching; if (was_watching) stopWatching(); auto urls = data->urls(); @@ -420,4 +395,42 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; } -#include "WorldList.moc" +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") { + return file.size(); + } else if (file.isDir()) { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void WorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) { + auto file = m_worlds.at(i).container(); + int row = i; + QThreadPool::globalInstance()->start([this, file, row]() mutable { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() { + if (row < m_worlds.size() && m_worlds[row].container() == file) { + m_worlds[row].setSize(size); + + // Notify views + QModelIndex modelIndex = index(row); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index bea24bb9a..93fecf1f5 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -40,9 +40,9 @@ class WorldList : public QAbstractListModel { virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; - size_t size() const { return worlds.size(); }; + size_t size() const { return m_worlds.size(); }; bool empty() const { return size() == 0; } - World& operator[](size_t index) { return worlds[index]; } + World& operator[](size_t index) { return m_worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); @@ -82,10 +82,11 @@ class WorldList : public QAbstractListModel { QString instDirPath() const; - const QList& allWorlds() const { return worlds; } + const QList& allWorlds() const { return m_worlds; } private slots: void directoryChanged(QString path); + void loadWorldsAsync(); signals: void changed(); @@ -93,7 +94,7 @@ class WorldList : public QAbstractListModel { protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; - bool is_watching; + bool m_isWatching; QDir m_dir; - QList worlds; + QList m_worlds; }; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index fd2082035..29a65e275 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -38,11 +38,10 @@ #include #include #include -#include #include namespace { -void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName) +void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) { if (!t.persistent) { return; @@ -181,6 +180,7 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); + out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); out.skin.variant = variantV.toString(); // data for skin is optional @@ -217,6 +217,7 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); + cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); cape.alias = aliasV.toString(); // data for cape is optional. diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 1ada4e38a..3b76012cb 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -36,12 +36,11 @@ #pragma once #include #include +#include #include -#include #include #include -#include #include enum class Validity { None, Assumed, Certain }; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 1c9ad395f..c53bf11a7 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -168,6 +168,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; @@ -260,6 +280,30 @@ int AccountList::count() const return m_accounts.count(); } +QString getAccountStatus(AccountState status) +{ + switch (status) { + case AccountState::Unchecked: + return QObject::tr("Unchecked", "Account status"); + case AccountState::Offline: + return QObject::tr("Offline", "Account status"); + case AccountState::Online: + return QObject::tr("Ready", "Account status"); + case AccountState::Working: + return QObject::tr("Working", "Account status"); + case AccountState::Errored: + return QObject::tr("Errored", "Account status"); + case AccountState::Expired: + return QObject::tr("Expired", "Account status"); + case AccountState::Disabled: + return QObject::tr("Disabled", "Account status"); + case AccountState::Gone: + return QObject::tr("Gone", "Account status"); + default: + return QObject::tr("Unknown", "Account status"); + } +} + QVariant AccountList::data(const QModelIndex& index, int role) const { if (!index.isValid()) @@ -271,15 +315,30 @@ QVariant AccountList::data(const QModelIndex& index, int role) const MinecraftAccountPtr account = at(index.row()); switch (role) { + case Qt::SizeHintRole: + if (index.column() == ProfileNameColumn) { + return QSize(0, 30); + } + + return QVariant(); + case Qt::DecorationRole: + if (index.column() == ProfileNameColumn) { + auto face = account->getFace(24, 24); + + if (!face.isNull()) { + return face; + } else { + return QIcon::fromTheme("noaccount").pixmap(24, 24); + } + } + + return QVariant(); case Qt::DisplayRole: switch (index.column()) { - case ProfileNameColumn: { + case ProfileNameColumn: return account->profileName(); - } - case NameColumn: return account->accountDisplayString(); - case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { @@ -291,39 +350,8 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } return tr("Unknown", "Account type"); } - - case StatusColumn: { - switch (account->accountState()) { - case AccountState::Unchecked: { - return tr("Unchecked", "Account status"); - } - case AccountState::Offline: { - return tr("Offline", "Account status"); - } - case AccountState::Online: { - return tr("Ready", "Account status"); - } - case AccountState::Working: { - return tr("Working", "Account status"); - } - case AccountState::Errored: { - return tr("Errored", "Account status"); - } - case AccountState::Expired: { - return tr("Expired", "Account status"); - } - case AccountState::Disabled: { - return tr("Disabled", "Account status"); - } - case AccountState::Gone: { - return tr("Gone", "Account status"); - } - default: { - return tr("Unknown", "Account status"); - } - } - } - + case StatusColumn: + return getAccountStatus(account->accountState()); default: return QVariant(); } @@ -335,11 +363,9 @@ QVariant AccountList::data(const QModelIndex& index, int role) const return QVariant::fromValue(account); case Qt::CheckStateRole: - if (index.column() == ProfileNameColumn) { + if (index.column() == ProfileNameColumn) return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); - } + return QVariant(); default: return QVariant(); @@ -461,18 +487,14 @@ bool AccountList::loadList() // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); - switch (listVersion) { - case AccountListVersion::MojangMSA: { - return loadV3(root); - } break; - default: { - QString newName = "accounts-old.json"; - qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; - // Attempt to rename the old version. - file.rename(newName); - return false; - } - } + if (listVersion == AccountListVersion::MojangMSA) + return loadV3(root); + + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; } bool AccountList::loadV3(QJsonObject& root) @@ -689,7 +711,7 @@ void AccountList::beginActivity() void AccountList::endActivity() { if (m_activityCount == 0) { - qWarning() << m_name << " - Activity count would become below zero"; + qWarning() << "Activity count would become below zero"; return; } bool deactivating = m_activityCount == 1; diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index d3be6740e..7438369a1 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -78,6 +78,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; @@ -111,7 +112,6 @@ class AccountList : public QAbstractListModel { void endActivity(); private: - const char* m_name; uint32_t m_activityCount = 0; signals: void listChanged(); diff --git a/launcher/minecraft/auth/AuthFlow.cpp b/launcher/minecraft/auth/AuthFlow.cpp index 19fbe15dd..ccdc7c0bf 100644 --- a/launcher/minecraft/auth/AuthFlow.cpp +++ b/launcher/minecraft/auth/AuthFlow.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -70,6 +69,7 @@ void AuthFlow::nextStep() } m_currentStep = m_steps.front(); qDebug() << "AuthFlow:" << m_currentStep->describe(); + setStatus(m_currentStep->describe()); m_steps.pop_front(); connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); @@ -93,7 +93,9 @@ bool AuthFlow::changeState(AccountTaskState newState, QString reason) return true; } case AccountTaskState::STATE_WORKING: { - setStatus(m_currentStep ? m_currentStep->describe() : tr("Working...")); + if (!m_currentStep) { + setStatus(tr("Preparing to log in...")); + } m_data->accountState = AccountState::Working; return true; } @@ -153,4 +155,4 @@ bool AuthFlow::abort() if (m_currentStep) m_currentStep->abort(); return true; -} \ No newline at end of file +} diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index bff4c04e4..710509d8e 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -5,7 +5,6 @@ #include #include #include -#include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp index 3657befec..a74eabb7a 100644 --- a/launcher/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -39,4 +39,4 @@ void AuthSession::MakeDemo(QString name, QString u) access_token = "0"; player_name = name; status = PlayableOnline; // needs online to download the assets -}; \ No newline at end of file +}; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 54e7d69e0..cbe604805 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -1,12 +1,9 @@ #pragma once -#include #include #include -#include "QObjectPtr.h" class MinecraftAccount; -class QNetworkAccessManager; struct AuthSession { bool MakeOffline(QString offline_playername); diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 1ed39b5ca..3a7de8a19 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,7 +55,8 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + static const QRegularExpression s_removeChars("[{}-]"); + data.internalId = QUuid::createUuid().toString().remove(s_removeChars); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -76,14 +77,15 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { + static const QRegularExpression s_removeChars("[{}-]"); auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); - account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(s_removeChars); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -99,22 +101,18 @@ 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")) { return QPixmap(); } QPixmap skin = QPixmap(8, 8); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) skin.fill(QColorConstants::Transparent); -#else - skin.fill(QColor(0, 0, 0, 0)); -#endif QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); - return skin.scaled(64, 64, Qt::KeepAspectRatio); + return skin.scaled(width, height, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) @@ -183,8 +181,10 @@ void MinecraftAccount::authFailed(QString reason) data.validity_ = Validity::None; emit changed(); } break; + case AccountTaskState::STATE_WORKING: { + data.accountState = AccountState::Unchecked; + } break; case AccountTaskState::STATE_CREATED: - case AccountTaskState::STATE_WORKING: case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } @@ -235,6 +235,7 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { + static const QRegularExpression s_removeChars("[{}-]"); if (ownsMinecraft() && !hasProfile()) { session->status = AuthSession::RequiresProfileSetup; } else { @@ -252,7 +253,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]")); + session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { @@ -290,13 +291,8 @@ QUuid MinecraftAccount::uuidFromUsername(QString username) // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - auto bOr = [](QByteArray& array, int index, char value) { array[index] = array.at(index) | value; }; - auto bAnd = [](QByteArray& array, int index, char value) { array[index] = array.at(index) & value; }; -#else auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; -#endif bAnd(digest, 6, (char)0x0f); // clear version bOr(digest, 6, (char)0x30); // set to version 3 bAnd(digest, 8, (char)0x3f); // clear variant diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index f6fcfada2..95f76ec9a 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -114,7 +114,7 @@ class MinecraftAccount : public QObject, public Usable { bool isActive() const; - [[nodiscard]] AccountType accountType() const noexcept { return data.type; } + AccountType accountType() const noexcept { return data.type; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } @@ -135,7 +135,7 @@ class MinecraftAccount : public QObject, public Usable { } } - QPixmap getFace() const; + QPixmap getFace(int width = 64, int height = 64) const; //! Returns the current state of the account AccountState accountState() const; diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index f9d89baa2..7eec879d3 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -75,7 +75,7 @@ bool getBool(QJsonValue value, bool& out) "Message":"", "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" } -// 2148916233 = missing XBox account +// 2148916233 = missing Xbox account // 2148916238 = child account not linked to a family */ @@ -207,6 +207,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(capeObj.value("url"), capeOut.url)) { continue; } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(capeObj.value("alias"), capeOut.alias)) { continue; } @@ -315,11 +316,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) auto value = pObj.value("value"); if (value.isString()) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); -#else - texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); -#endif } if (!texturePayload.isEmpty()) { @@ -362,6 +359,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Skin url is not a string"; return false; } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); auto maybeMeta = skin.find("metadata"); if (maybeMeta != skin.end() && maybeMeta->isObject()) { @@ -375,6 +373,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Cape url is not a string"; return false; } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); // we don't know the cape ID as it is not returned from the session server // so just fake it - changing capes is probably locked anyway :( diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index 5b9809c52..890c1d77b 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -32,8 +32,8 @@ void EntitlementsStep::perform() { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Download::makeByteArray(url, m_response.get()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.h b/launcher/minecraft/auth/steps/EntitlementsStep.h index f20fcac08..b6d077004 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.h +++ b/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -22,7 +22,7 @@ class EntitlementsStep : public AuthStep { private: QString m_entitlements_request_id; - std::shared_ptr m_response; + std::unique_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index e067bc34c..ac83119e9 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -17,7 +17,7 @@ void GetSkinStep::perform() QUrl url(m_data->minecraftProfile.skin.url); m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); + m_request = Net::Download::makeByteArray(url, m_response.get()); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/GetSkinStep.h b/launcher/minecraft/auth/steps/GetSkinStep.h index c598f05d9..11a0820fd 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.h +++ b/launcher/minecraft/auth/steps/GetSkinStep.h @@ -21,7 +21,7 @@ class GetSkinStep : public AuthStep { void onRequestDone(); private: - std::shared_ptr m_response; + std::unique_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index 954f013af..ab4748bca 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -14,7 +14,7 @@ LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} QString LauncherLoginStep::describe() { - return tr("Accessing Mojang services."); + return tr("Fetching Minecraft access token"); } void LauncherLoginStep::perform() @@ -37,8 +37,8 @@ void LauncherLoginStep::perform() }; m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Upload::makeByteArray(url, m_response.get(), requestBody.toUtf8()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -69,5 +69,5 @@ void LauncherLoginStep::onRequestDone() emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; } - emit finished(AccountTaskState::STATE_WORKING, tr("")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft access token")); } diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.h b/launcher/minecraft/auth/steps/LauncherLoginStep.h index 0b5969f2b..7ea06ee1e 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.h +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -21,7 +21,7 @@ class LauncherLoginStep : public AuthStep { void onRequestDone(); private: - std::shared_ptr m_response; + std::unique_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 38ff90a47..19088e64b 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -67,8 +67,8 @@ void MSADeviceCodeStep::perform() { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Upload::makeByteArray(url, m_response.get(), payload); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -105,9 +105,8 @@ DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& d } auto obj = doc.object(); return { - Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), Json::ensureString(obj, "verification_uri"), - Json::ensureInteger(obj, "expires_in"), Json::ensureInteger(obj, "interval"), Json::ensureString(obj, "error"), - Json::ensureString(obj, "error_description"), + obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(), + obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(), }; } @@ -182,8 +181,8 @@ void MSADeviceCodeStep::authenticateUser() { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, payload); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Upload::makeByteArray(url, m_response.get(), payload); + m_request->addHeaderProxy(std::make_unique(headers)); connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); @@ -217,12 +216,12 @@ AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) return {}; } auto obj = doc.object(); - return { Json::ensureString(obj, "access_token"), - Json::ensureString(obj, "token_type"), - Json::ensureString(obj, "refresh_token"), - Json::ensureInteger(obj, "expires_in"), - Json::ensureString(obj, "error"), - Json::ensureString(obj, "error_description"), + return { obj["access_token"].toString(), + obj["token_type"].toString(), + obj["refresh_token"].toString(), + obj["expires_in"].toInt(), + obj["error"].toString(), + obj["error_description"].toString(), obj.toVariantMap() }; } @@ -273,5 +272,5 @@ void MSADeviceCodeStep::authenticationFinished() m_data->msaToken.extra = rsp.extra; m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; - emit finished(AccountTaskState::STATE_WORKING, tr("Got")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); } diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h index 7f755563f..62fbbc7a5 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.h +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.h @@ -72,7 +72,7 @@ class MSADeviceCodeStep : public AuthStep { QTimer m_pool_timer; QTimer m_expiration_timer; - std::shared_ptr m_response; + std::unique_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 87a0f8f08..0c5412ca8 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -107,7 +107,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(); @@ -116,7 +116,7 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile m_data->msaToken.extra = m_oauth2.extraTokens(); m_data->msaToken.refresh_token = m_oauth2.refreshToken(); m_data->msaToken.token = m_oauth2.token(); - emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) { @@ -168,13 +168,8 @@ void MSAStep::perform() m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); m_oauth2.refreshAccessToken(); } else { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 m_oauth2.setModifyParametersFunction( [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); -#else - m_oauth2.setModifyParametersFunction( - [](QAbstractOAuth::Stage stage, QMap* map) { map->insert("prompt", "select_account"); }); -#endif *m_data = AccountData(); m_data->msaClientID = m_clientId; @@ -182,4 +177,4 @@ void MSAStep::perform() } } -#include "MSAStep.moc" \ No newline at end of file +#include "MSAStep.moc" diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index c1529b086..4fe18c98e 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -22,8 +22,8 @@ void MinecraftProfileStep::perform() { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Download::makeByteArray(url, m_response.get()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -66,5 +66,5 @@ void MinecraftProfileStep::onRequestDone() return; } - emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded.")); + emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft profile")); } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h index e8b35b875..d6de74731 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.h +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -21,7 +21,7 @@ class MinecraftProfileStep : public AuthStep { void onRequestDone(); private: - std::shared_ptr m_response; + std::unique_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 f9de9210b..aac31a04c 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -42,8 +42,8 @@ void XboxAuthorizationStep::perform() { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); - m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Upload::makeByteArray(url, m_response.get(), xbox_auth_data.toUtf8()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -115,13 +115,13 @@ bool XboxAuthorizationStep::processSTSError() switch (errorCode) { case 2148916233: { emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + tr("This Microsoft account does not have an Xbox Live profile. Buy the game on %1 first.") .arg("minecraft.net")); return true; } case 2148916235: { // NOTE: this is the Grulovia error - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox Live is not available in your country. You've been blocked.")); + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox Live is not available in your country. You've been blocked.")); return true; } case 2148916238: { diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h index 8418727c4..a152ed5e6 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.h +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -28,7 +28,7 @@ class XboxAuthorizationStep : public AuthStep { QString m_relyingParty; QString m_authorizationKind; - std::shared_ptr m_response; + std::unique_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp index b896357c0..95d9eb854 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -34,8 +34,8 @@ void XboxProfileStep::perform() }; m_response.reset(new QByteArray()); - m_request = Net::Download::makeByteArray(url, m_response); - m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request = Net::Download::makeByteArray(url, m_response.get()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("XboxProfileStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -60,7 +60,7 @@ void XboxProfileStep::onRequestDone() return; } - qCDebug(authCredentials()) << "XBox profile: " << *m_response; + qCDebug(authCredentials()) << "Xbox profile: " << *m_response; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); } diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.h b/launcher/minecraft/auth/steps/XboxProfileStep.h index f2ab874f2..d447f7a2b 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.h +++ b/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -21,7 +21,7 @@ class XboxProfileStep : public AuthStep { void onRequestDone(); private: - std::shared_ptr m_response; + std::unique_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 4e5abb62b..a48997177 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -38,8 +38,8 @@ void XboxUserStep::perform() { "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)); + m_request = Net::Upload::makeByteArray(url, m_response.get(), xbox_auth_data.toUtf8()); + m_request->addHeaderProxy(std::make_unique(headers)); m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); m_task->setAskRetry(false); @@ -48,7 +48,7 @@ void XboxUserStep::perform() connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone); m_task->start(); - qDebug() << "First layer of XBox auth ... commencing."; + qDebug() << "First layer of Xbox auth ... commencing."; } void XboxUserStep::onRequestDone() @@ -56,9 +56,9 @@ void XboxUserStep::onRequestDone() if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error())) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed: %1").arg(m_request->errorString())); + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } else { - emit finished(AccountTaskState::STATE_OFFLINE, tr("XBox user authentication failed: %1").arg(m_request->errorString())); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } return; } @@ -66,7 +66,7 @@ void XboxUserStep::onRequestDone() Token temp; if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication response could not be understood.")); return; } m_data->userToken = temp; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.h b/launcher/minecraft/auth/steps/XboxUserStep.h index f6cc822f2..e3c31ae9d 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.h +++ b/launcher/minecraft/auth/steps/XboxUserStep.h @@ -21,7 +21,7 @@ class XboxUserStep : public AuthStep { void onRequestDone(); private: - std::shared_ptr m_response; + std::unique_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; diff --git a/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp deleted file mode 100644 index 4f4fb99a7..000000000 --- a/launcher/minecraft/gameoptions/GameOptions.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "GameOptions.h" -#include -#include -#include "FileSystem.h" - -namespace { -bool load(const QString& path, std::vector& contents, int& version) -{ - contents.clear(); - QFile file(path); - if (!file.open(QFile::ReadOnly)) { - qWarning() << "Failed to read options file."; - return false; - } - version = 0; - while (!file.atEnd()) { - auto line = file.readLine(); - if (line.endsWith('\n')) { - line.chop(1); - } - auto separatorIndex = line.indexOf(':'); - if (separatorIndex == -1) { - continue; - } - auto key = QString::fromUtf8(line.data(), separatorIndex); - auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); - qDebug() << "!!" << key << "!!"; - if (key == "version") { - version = value.toInt(); - continue; - } - contents.emplace_back(GameOptionItem{ key, value }); - } - qDebug() << "Loaded" << path << "with version:" << version; - return true; -} -bool save(const QString& path, std::vector& mapping, int version) -{ - QSaveFile out(path); - if (!out.open(QIODevice::WriteOnly)) { - return false; - } - if (version != 0) { - QString versionLine = QString("version:%1\n").arg(version); - out.write(versionLine.toUtf8()); - } - auto iter = mapping.begin(); - while (iter != mapping.end()) { - out.write(iter->key.toUtf8()); - out.write(":"); - out.write(iter->value.toUtf8()); - out.write("\n"); - iter++; - } - return out.commit(); -} -} // namespace - -GameOptions::GameOptions(const QString& path) : path(path) -{ - reload(); -} - -QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (role != Qt::DisplayRole) { - return QAbstractListModel::headerData(section, orientation, role); - } - switch (section) { - case 0: - return tr("Key"); - case 1: - return tr("Value"); - default: - return QVariant(); - } -} - -QVariant GameOptions::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= int(contents.size())) - return QVariant(); - - switch (role) { - case Qt::DisplayRole: - if (column == 0) { - return contents[row].key; - } else { - return contents[row].value; - } - default: - return QVariant(); - } -} - -int GameOptions::rowCount(const QModelIndex&) const -{ - return static_cast(contents.size()); -} - -int GameOptions::columnCount(const QModelIndex&) const -{ - return 2; -} - -bool GameOptions::isLoaded() const -{ - return loaded; -} - -bool GameOptions::reload() -{ - beginResetModel(); - loaded = load(path, contents, version); - endResetModel(); - return loaded; -} - -bool GameOptions::save() -{ - return ::save(path, contents, version); -} diff --git a/launcher/minecraft/gameoptions/GameOptions.h b/launcher/minecraft/gameoptions/GameOptions.h deleted file mode 100644 index ae031efb2..000000000 --- a/launcher/minecraft/gameoptions/GameOptions.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -struct GameOptionItem { - QString key; - QString value; -}; - -class GameOptions : public QAbstractListModel { - Q_OBJECT - public: - explicit GameOptions(const QString& path); - virtual ~GameOptions() = default; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - bool isLoaded() const; - bool reload(); - bool save(); - - private: - std::vector contents; - bool loaded = false; - QString path; - int version = 0; -}; diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index 854590dd2..4f2d1f0c3 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -109,7 +109,8 @@ void AutoInstallJava::executeTask() return; } QDir javaDir(APPLICATION->javaPath()); - auto wantedJavaPath = javaDir.absoluteFilePath(wantedJavaName); + auto relativeBinary = FS::PathCombine(wantedJavaName, "bin", JavaUtils::javaExecutable); + auto wantedJavaPath = javaDir.absoluteFilePath(relativeBinary); if (QFileInfo::exists(wantedJavaPath)) { setJavaPathFromPartial(); return; @@ -165,6 +166,7 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) if (java->runtimeOS == m_supported_arch && java->name() == javaName) { QDir javaDir(APPLICATION->javaPath()); auto final_path = javaDir.absoluteFilePath(java->m_name); + auto deletePath = [final_path] { FS::deletePath(final_path); }; switch (java->downloadType) { case Java::DownloadType::Manifest: m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); @@ -174,6 +176,7 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) break; case Java::DownloadType::Unknown: emitFailed(tr("Could not determine Java download type!")); + deletePath(); return; } #if defined(Q_OS_MACOS) @@ -182,12 +185,11 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) seq->addTask(makeShared(final_path)); m_current_task = seq; #endif - auto deletePath = [final_path] { FS::deletePath(final_path); }; connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { deletePath(); emitFailed(reason); }); - connect(m_current_task.get(), &Task::aborted, this, [deletePath] { deletePath(); }); + connect(m_current_task.get(), &Task::aborted, this, deletePath); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); 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..eecc5282b 100644 --- a/launcher/minecraft/launch/ClaimAccount.cpp +++ b/launcher/minecraft/launch/ClaimAccount.cpp @@ -15,7 +15,7 @@ ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session) : LaunchS void ClaimAccount::executeTask() { if (m_account) { - lock.reset(new UseLock(m_account)); + lock.reset(new UseLock(m_account.get())); emitSucceeded(); } } diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index afe091758..a7d47f1a1 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -17,11 +17,10 @@ #include #include -#include -#include #include #include "FileSystem.h" -#include "MMCZip.h" +#include "archive/ArchiveReader.h" +#include "archive/ArchiveWriter.h" #ifdef major #undef major @@ -41,30 +40,21 @@ static QString replaceSuffix(QString target, const QString& suffix, const QStrin static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) { - QuaZip zip(source); - if (!zip.open(QuaZip::mdUnzip)) { - return false; - } + MMCZip::ArchiveReader zip(source); QDir directory(targetFolder); - if (!zip.goToFirstFile()) { - return false; - } - do { - QString name = zip.getCurrentFileName(); + + auto extPtr = MMCZip::ArchiveWriter::createDiskWriter(); + auto ext = extPtr.get(); + + return zip.parse([applyJnilibHack, directory, ext](MMCZip::ArchiveReader::File* f) { + QString name = f->filename(); auto lowercase = name.toLower(); if (applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); - if (!JlCompress::extractFile(&zip, "", absFilePath)) { - return false; - } - } while (zip.goToNextFile()); - zip.close(); - if (zip.getZipError() != 0) { - return false; - } - return true; + return f->writeFile(ext, absFilePath); + }); } void ExtractNatives::executeTask() @@ -75,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 5a13d18cf..d966287af 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -50,14 +50,15 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) - , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) + , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QStringConverter::Utf8 : QStringConverter::System) { if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { + static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); std::shared_ptr connection{ new QMetaObject::Connection }; *connection = - connect(&m_process, &LoggedProcess::log, this, [=](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + connect(&m_process, &LoggedProcess::log, this, [connection](const QStringList& lines, [[maybe_unused]] MessageLevel level) { qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { + if (lines.filter(s_settingUser).length() != 0) { APPLICATION->closeAllWindows(); disconnect(*connection); } 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/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 4a9e77a70..e089f2f6e 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -25,7 +25,9 @@ #include #include +#include "MTPixmapCache.h" #include "Version.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats @@ -93,6 +95,51 @@ void DataPack::setDescription(QString new_description) m_description = new_description; } +void DataPack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::instance().remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } +} + +QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) { + return {}; + } else { + qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + DataPackUtils::processPackPNG(this); + return image(size); +} + std::pair DataPack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { @@ -105,19 +152,16 @@ std::pair DataPack::compatibleVersions() const int DataPack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); - switch (type) { - default: - return Resource::compare(other, type); - case SortType::PACK_FORMAT: { - auto this_ver = packFormat(); - auto other_ver = cast_other.packFormat(); - - if (this_ver > other_ver) - return 1; - if (this_ver < other_ver) - return -1; - break; - } + if (type == SortType::PACK_FORMAT) { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + } else { + return Resource::compare(other, type); } return 0; } diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 4855b0203..2943dd4bc 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -24,6 +24,7 @@ #include "Resource.h" #include +#include class Version; @@ -35,18 +36,19 @@ class Version; class DataPack : public Resource { Q_OBJECT public: - using Ptr = shared_qobject_ptr; - DataPack(QObject* parent = nullptr) : Resource(parent) {} DataPack(QFileInfo file_info) : Resource(file_info) {} /** Gets the numerical ID of the pack format. */ - [[nodiscard]] int packFormat() const { return m_pack_format; } + int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] std::pair compatibleVersions() const; + virtual std::pair compatibleVersions() const; /** Gets the description of the data pack. */ - [[nodiscard]] QString description() const { return m_description; } + QString description() const { return m_description; } + + /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id); @@ -54,6 +56,9 @@ class DataPack : public Resource { /** Thread-safe. */ void setDescription(QString new_description); + /** Thread-safe. */ + void setImage(QImage new_image) const; + bool valid() const override; [[nodiscard]] int compare(Resource const& other, SortType type) const override; @@ -70,4 +75,14 @@ class DataPack : public Resource { /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; + + /** The data pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp new file mode 100644 index 000000000..7abab7a06 --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 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 "DataPackFolderModel.h" + +#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) +{ + 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 }; +} + +QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const +{ + 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(); + + default: + return {}; + } + 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 {}; + } + 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(); + } + 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 {}; + default: + return {}; + } +} + +QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: + return columnNames().at(section); + default: + return {}; + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the data pack enabled? (Only valid for ZIPs)"); + case NameColumn: + return tr("The name of the data pack."); + case 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."); + case DateColumn: + return tr("The date and time this data pack was last changed (or added)."); + default: + return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) { + return QSize(64, 0); + } + return {}; + default: + return {}; + } +} + +int DataPackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Resource* DataPackFolderModel::createResource(const QFileInfo& file) +{ + return new DataPack(file); +} + +Task* DataPackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); +} diff --git a/launcher/minecraft/mod/DataPackFolderModel.h b/launcher/minecraft/mod/DataPackFolderModel.h new file mode 100644 index 000000000..2b90e1a2a --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 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 "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 }; + + explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + + virtual QString id() const override { return "datapacks"; } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override; + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(DataPack) +}; diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index fb3a10133..5f12348ae 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -19,40 +19,34 @@ #pragma once -#include - #include "modplatform/packwiz/Packwiz.h" -// launcher/minecraft/mod/Mod.h -class Mod; - -/* Abstraction file for easily changing the way metadata is stored / handled - * Needs to be a class because of -Wunused-function and no C++17 [[maybe_unused]] - * */ -class Metadata { - public: - using ModStruct = Packwiz::V1::Mod; - using ModSide = Packwiz::V1::Side; - - static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct - { - return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); - } - - static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct - { - return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug); - } +namespace Metadata { +using ModStruct = Packwiz::V1::Mod; - static void update(QDir& index_dir, ModStruct& mod) { Packwiz::V1::updateModIndex(index_dir, mod); } +inline ModStruct create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) +{ + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); +} - static void remove(QDir& index_dir, QString mod_slug) { Packwiz::V1::deleteModIndex(index_dir, mod_slug); } +inline void update(const QDir& index_dir, ModStruct& mod) +{ + Packwiz::V1::updateModIndex(index_dir, mod); +} - static void remove(QDir& index_dir, QVariant& mod_id) { Packwiz::V1::deleteModIndex(index_dir, mod_id); } +inline void remove(const QDir& index_dir, QString mod_slug) +{ + Packwiz::V1::deleteModIndex(index_dir, mod_slug); +} - static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_slug); } +inline ModStruct get(const QDir& index_dir, QString mod_slug) +{ + return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); +} - static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } +inline ModStruct get(const QDir& index_dir, QVariant& mod_id) +{ + return Packwiz::V1::getIndexForMod(index_dir, mod_id); +} - static auto modSideToString(ModSide side) -> QString { return Packwiz::V1::sideToString(side); } -}; +}; // namespace Metadata diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 9663026cd..32b940c5b 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -36,18 +36,16 @@ */ #include "Mod.h" -#include -#include #include #include #include #include "MTPixmapCache.h" #include "MetadataHandler.h" +#include "Resource.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" -#include "minecraft/mod/Resource.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" @@ -56,24 +54,6 @@ Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() m_enabled = (file.suffix() != "disabled"); } -Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata) : Mod(mods_dir.absoluteFilePath(metadata.filename)) -{ - m_name = metadata.name; - m_local_details.metadata = std::make_shared(std::move(metadata)); -} - -void Mod::setStatus(ModStatus status) -{ - m_local_details.status = status; -} -void Mod::setMetadata(std::shared_ptr&& metadata) -{ - if (status() == ModStatus::NoMetadata) - setStatus(ModStatus::Installed); - - m_local_details.metadata = metadata; -} - void Mod::setDetails(const ModDetails& details) { m_local_details = details; @@ -101,33 +81,28 @@ int Mod::compare(const Resource& other, SortType type) const return -1; break; } - case SortType::PROVIDER: { - return QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); - } case SortType::SIDE: { - if (side() > cast_other->side()) - return 1; - else if (side() < cast_other->side()) - return -1; + auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } - case SortType::LOADERS: { - if (loaders() > cast_other->loaders()) - return 1; - else if (loaders() < cast_other->loaders()) - return -1; + case SortType::MC_VERSIONS: { + auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } - case SortType::MC_VERSIONS: { - auto thisVersion = mcVersions().join(","); - auto otherVersion = cast_other->mcVersions().join(","); - return QString::compare(thisVersion, otherVersion, Qt::CaseInsensitive); + case SortType::LOADERS: { + auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; } case SortType::RELEASE_TYPE: { - if (releaseType() > cast_other->releaseType()) - return 1; - else if (releaseType() < cast_other->releaseType()) - return -1; + auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; break; } } @@ -148,28 +123,6 @@ bool Mod::applyFilter(QRegularExpression filter) const return Resource::applyFilter(filter); } -auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool -{ - if (!preserve_metadata) { - qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); - - destroyMetadata(index_dir); - } - - return Resource::destroy(attempt_trash); -} - -void Mod::destroyMetadata(QDir& index_dir) -{ - if (metadata()) { - Metadata::remove(index_dir, metadata()->slug); - } else { - auto n = name(); - Metadata::remove(index_dir, n); - } - m_local_details.metadata = nullptr; -} - auto Mod::details() const -> const ModDetails& { return m_local_details; @@ -181,10 +134,16 @@ auto Mod::name() const -> QString if (!d_name.isEmpty()) return d_name; - if (metadata()) - return metadata()->name; + return Resource::name(); +} + +auto Mod::mod_id() const -> QString +{ + auto d_mod_id = details().mod_id; + if (!d_mod_id.isEmpty()) + return d_mod_id; - return m_name; + return Resource::name(); } auto Mod::version() const -> QString @@ -192,41 +151,62 @@ auto Mod::version() const -> QString return details().version; } -auto Mod::homeurl() const -> QString +auto Mod::homepage() const -> QString { - return details().homeurl; + QString metaUrl = Resource::homepage(); + + if (metaUrl.isEmpty()) + return details().homeurl; + else + return metaUrl; } -auto Mod::metaurl() const -> QString +auto Mod::loaders() const -> QString { - if (metadata() == nullptr) - return homeurl(); - return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + if (metadata()) { + QStringList loaders; + auto modLoaders = metadata()->loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) { + loaders << getModLoaderAsString(loader); + } + return loaders.join(", "); + } + + return {}; } -auto Mod::description() const -> QString +auto Mod::side() const -> QString { - return details().description; + if (metadata()) + return ModPlatform::SideUtils::toString(metadata()->side); + + return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); } -auto Mod::authors() const -> QStringList +auto Mod::mcVersions() const -> QString { - return details().authors; + if (metadata()) + return metadata()->mcVersions.join(", "); + + return {}; } -auto Mod::status() const -> ModStatus +auto Mod::releaseType() const -> QString { - return details().status; + if (metadata()) + return metadata()->releaseType.toString(); + + return ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::Unknown).toString(); } -auto Mod::metadata() -> std::shared_ptr +auto Mod::description() const -> QString { - return m_local_details.metadata; + return details().description; } -auto Mod::metadata() const -> const std::shared_ptr +auto Mod::authors() const -> QStringList { - return m_local_details.metadata; + return details().authors; } void Mod::finishResolvingWithDetails(ModDetails&& details) @@ -234,53 +214,12 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_is_resolving = false; m_is_resolved = true; - std::shared_ptr metadata = details.metadata; - if (details.status == ModStatus::Unknown) - details.status = m_local_details.status; - m_local_details = std::move(details); - if (metadata) - setMetadata(std::move(metadata)); if (!iconPath().isEmpty()) { m_packImageCacheKey.wasReadAttempt = false; } } -auto Mod::provider() const -> std::optional -{ - if (metadata()) - return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); - return {}; -} - -auto Mod::side() const -> Metadata::ModSide -{ - if (metadata()) - return metadata()->side; - return Metadata::ModSide::UniversalSide; -} - -auto Mod::releaseType() const -> ModPlatform::IndexedVersionType -{ - if (metadata()) - return metadata()->releaseType; - return ModPlatform::IndexedVersionType::VersionType::Unknown; -} - -auto Mod::loaders() const -> ModPlatform::ModLoaderTypes -{ - if (metadata()) - return metadata()->loaders; - return {}; -} - -auto Mod::mcVersions() const -> QStringList -{ - if (metadata()) - return metadata()->mcVersions; - return {}; -} - auto Mod::licenses() const -> const QList& { return details().licenses; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index a0d9797ed..c548f5350 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -44,11 +44,8 @@ #include #include -#include - #include "ModDetails.h" #include "Resource.h" -#include "modplatform/ModIndex.h" class Mod : public Resource { Q_OBJECT @@ -58,43 +55,34 @@ class Mod : public Resource { Mod() = default; Mod(const QFileInfo& file); - Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); Mod(QString file_path) : Mod(QFileInfo(file_path)) {} auto details() const -> const ModDetails&; auto name() const -> QString override; + auto mod_id() const -> QString; auto version() const -> QString; - auto homeurl() const -> QString; + auto homepage() const -> QString override; auto description() const -> QString; auto authors() const -> QStringList; - auto status() const -> ModStatus; - auto provider() const -> std::optional; auto licenses() const -> const QList&; auto issueTracker() const -> QString; - auto metaurl() const -> QString; - auto side() const -> Metadata::ModSide; - auto loaders() const -> ModPlatform::ModLoaderTypes; - auto mcVersions() const -> QStringList; - auto releaseType() const -> ModPlatform::IndexedVersionType; + auto side() const -> QString; + auto loaders() const -> QString; + auto mcVersions() const -> QString; + auto releaseType() const -> QString; /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ QPixmap setIcon(QImage new_image) const; - auto metadata() -> std::shared_ptr; - auto metadata() const -> const std::shared_ptr; - - void setStatus(ModStatus status); - void setMetadata(std::shared_ptr&& metadata); - void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } void setDetails(const ModDetails& details); bool valid() const override; - [[nodiscard]] int compare(Resource const& other, SortType type) const override; + [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index a00d5a24b..9b81f561f 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -35,21 +35,10 @@ #pragma once -#include - #include #include #include -#include "minecraft/mod/MetadataHandler.h" - -enum class ModStatus { - Installed, // Both JAR and Metadata are present - NotInstalled, // Only the Metadata is present - NoMetadata, // Only the JAR is present - Unknown, // Default status -}; - struct ModLicense { QString name = {}; QString id = {}; @@ -149,12 +138,6 @@ struct ModDetails { /* Path of mod logo */ QString icon_file = {}; - /* Installation status of the mod */ - ModStatus status = ModStatus::Unknown; - - /* Metadata information, if any */ - std::shared_ptr metadata = nullptr; - ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ @@ -169,40 +152,9 @@ struct ModDetails { , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) - , status(other.status) {} - ModDetails& operator=(const ModDetails& other) - { - this->mod_id = other.mod_id; - this->name = other.name; - this->version = other.version; - this->mcversion = other.mcversion; - this->homeurl = other.homeurl; - this->description = other.description; - this->authors = other.authors; - this->issue_tracker = other.issue_tracker; - this->licenses = other.licenses; - this->icon_file = other.icon_file; - this->status = other.status; - - return *this; - } + ModDetails& operator=(const ModDetails& other) = default; - ModDetails& operator=(const ModDetails&& other) - { - this->mod_id = other.mod_id; - this->name = other.name; - this->version = other.version; - this->mcversion = other.mcversion; - this->homeurl = other.homeurl; - this->description = other.description; - this->authors = other.authors; - this->issue_tracker = other.issue_tracker; - this->licenses = other.licenses; - this->icon_file = other.icon_file; - this->status = other.status; - - return *this; - } + ModDetails& operator=(ModDetails&& other) = default; }; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 5e4fe7f11..45ec76f19 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -49,20 +49,10 @@ #include #include -#include "Application.h" - -#include "Json.h" -#include "minecraft/mod/MetadataHandler.h" -#include "minecraft/mod/Resource.h" #include "minecraft/mod/tasks/LocalModParseTask.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" -#include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" -#include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlameModIndex.h" -ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) - : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", "Minecraft Versions", "Release Type" }); @@ -75,7 +65,6 @@ ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool 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_columnsHiddenByDefault = { false, false, false, false, false, false, false, true, true, true, true }; } QVariant ModFolderModel::data(const QModelIndex& index, int role) const @@ -92,7 +81,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const case NameColumn: return m_resources[row]->name(); case VersionColumn: { - switch (m_resources[row]->type()) { + switch (at(row).type()) { case ResourceType::FOLDER: return tr("Folder"); case ResourceType::SINGLEFILE: @@ -100,64 +89,50 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const default: break; } - return at(row)->version(); + return at(row).version(); } case DateColumn: - return m_resources[row]->dateTimeChanged(); + return at(row).dateTimeChanged(); case ProviderColumn: { - auto provider = at(row)->provider(); - if (!provider.has_value()) { - //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) - return tr("Unknown"); - } - - return provider.value(); + return at(row).provider(); } case SideColumn: { - return Metadata::modSideToString(at(row)->side()); + return at(row).side(); } case LoadersColumn: { - QStringList loaders; - auto modLoaders = at(row)->loaders(); - for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, - ModPlatform::Fabric, ModPlatform::Quilt }) { - if (modLoaders & loader) { - loaders << getModLoaderAsString(loader); - } - } - return loaders.join(", "); + return at(row).loaders(); } case McVersionsColumn: { - return at(row)->mcVersions().join(", "); + return at(row).mcVersions(); } case ReleaseTypeColumn: { - return at(row)->releaseType().toString(); + return at(row).releaseType(); } case SizeColumn: - return m_resources[row]->sizeStr(); + return at(row).sizeStr(); default: return QVariant(); } case Qt::ToolTipRole: if (column == NameColumn) { - if (at(row)->isSymLinkUnder(instDirPath())) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + 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 at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -167,12 +142,9 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const } return {}; case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); default: return QVariant(); } @@ -210,7 +182,7 @@ QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientatio case DateColumn: return tr("The date and time this mod was last changed (or added)."); case ProviderColumn: - return tr("Where the mod was downloaded from."); + return tr("The source provider of the mod."); case SideColumn: return tr("On what environment the mod is running."); case LoadersColumn: @@ -235,133 +207,16 @@ int ModFolderModel::columnCount(const QModelIndex& parent) const return parent.isValid() ? 0 : NUM_COLUMNS; } -Task* ModFolderModel::createUpdateTask() -{ - auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); - m_first_folder_load = false; - return task; -} - Task* ModFolderModel::createParseTask(Resource& resource) { return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } -bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) -{ - for (auto mod : allMods()) { - if (mod->getOriginalFileName() == filename) { - auto index_dir = indexDir(); - mod->destroy(index_dir, preserve_metadata, false); - - update(); - - return true; - } - } - - return false; -} - -bool ModFolderModel::deleteMods(const QModelIndexList& indexes) -{ - if (indexes.isEmpty()) - return true; - - for (auto i : indexes) { - if (i.column() != 0) { - continue; - } - auto m = at(i.row()); - auto index_dir = indexDir(); - m->destroy(index_dir); - } - - update(); - - return true; -} - -bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes) -{ - if (indexes.isEmpty()) - return true; - - for (auto i : indexes) { - if (i.column() != 0) { - continue; - } - auto m = at(i.row()); - auto index_dir = indexDir(); - m->destroyMetadata(index_dir); - } - - update(); - - return true; -} - bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); } -bool ModFolderModel::startWatching() -{ - // Remove orphaned metadata next time - m_first_folder_load = true; - return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); -} - -bool ModFolderModel::stopWatching() -{ - return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() }); -} - -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList -{ - QList selected_resources; - for (auto i : indexes) { - if (i.column() != 0) - continue; - - selected_resources.push_back(at(i.row())); - } - return selected_resources; -} - -auto ModFolderModel::allMods() -> QList -{ - QList mods; - - for (auto& res : qAsConst(m_resources)) { - mods.append(static_cast(res.get())); - } - - return mods; -} - -void ModFolderModel::onUpdateSucceeded() -{ - auto update_results = static_cast(m_current_update_task.get())->result(); - - auto& new_mods = update_results->mods; - -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto current_list = m_resources_index.keys(); - QSet current_set(current_list.begin(), current_list.end()); - - auto new_list = new_mods.keys(); - QSet new_set(new_list.begin(), new_list.end()); -#else - QSet current_set(m_resources_index.keys().toSet()); - QSet new_set(new_mods.keys().toSet()); -#endif - - applyUpdates(current_set, new_set, new_mods); -} - void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { auto iter = m_active_parse_tasks.constFind(ticket); @@ -379,51 +234,7 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) auto result = cast_task->result(); if (result && resource) - resource->finishResolvingWithDetails(std::move(result->details)); + static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } - -static const FlameAPI flameAPI; -bool ModFolderModel::installMod(QString file_path, ModPlatform::IndexedVersion& vers) -{ - if (vers.addonId.isValid()) { - ModPlatform::IndexedPack pack{ - vers.addonId, - ModPlatform::ResourceProvider::FLAME, - }; - - QEventLoop loop; - - auto response = std::make_shared(); - auto job = flameAPI.getProject(vers.addonId.toString(), response); - - QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); }); - QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit); - QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qDebug() << *response; - return; - } - try { - auto obj = Json::requireObject(Json::requireObject(doc), "data"); - FlameMod::loadIndexedPack(pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading mod info: " << e.cause(); - } - LocalModUpdateTask update_metadata(indexDir(), pack, vers); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - update_metadata.start(); - }); - - job->start(); - - loop.exec(); - } - return ResourceFolderModel::installResource(file_path); -} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index db5edf66e..42868dc91 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -47,11 +47,6 @@ #include "Mod.h" #include "ResourceFolderModel.h" -#include "minecraft/mod/tasks/LocalModParseTask.h" -#include "minecraft/mod/tasks/ModFolderLoadTask.h" -#include "modplatform/ModIndex.h" - -class LegacyInstance; class BaseInstance; class QFileSystemWatcher; @@ -76,8 +71,7 @@ class ModFolderModel : public ResourceFolderModel { ReleaseTypeColumn, NUM_COLUMNS }; - enum ModStatusAction { Disable, Enable, Toggle }; - ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); + ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "mods"; } @@ -86,34 +80,13 @@ class ModFolderModel : public ResourceFolderModel { QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } [[nodiscard]] Task* createParseTask(Resource&) override; - bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } - bool installMod(QString file_path, ModPlatform::IndexedVersion& vers); - bool uninstallMod(const QString& filename, bool preserve_metadata = false); - - /// Deletes all the selected mods - bool deleteMods(const QModelIndexList& indexes); - bool deleteModsMetadata(const QModelIndexList& indexes); - bool isValid(); - bool startWatching() override; - bool stopWatching() override; - - QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; } - - auto selectedMods(QModelIndexList& indexes) -> QList; - auto allMods() -> QList; - RESOURCE_HELPERS(Mod) private slots: - void onUpdateSucceeded() override; void onParseSucceeded(int ticket, QString resource_id) override; - - protected: - bool m_is_indexed; - bool m_first_folder_load = true; }; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index c52d76f1f..8f67d5d94 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -21,7 +21,7 @@ void Resource::setFile(QFileInfo file_info) parseFile(); } -std::tuple calculateFileSize(const QFileInfo& file) +static std::tuple calculateFileSize(const QFileInfo& file) { if (file.isDir()) { auto dir = QDir(file.absoluteFilePath()); @@ -72,13 +72,45 @@ void Resource::parseFile() m_changed_date_time = m_file_info.lastModified(); } +auto Resource::name() const -> QString +{ + if (metadata()) + return metadata()->name; + + return m_name; +} + static void removeThePrefix(QString& string) { - QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); - string.remove(regex); + static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); + string.remove(s_regex); string = string.trimmed(); } +auto Resource::provider() const -> QString +{ + if (metadata()) + return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); + + return tr("Unknown"); +} + +auto Resource::homepage() const -> QString +{ + if (metadata()) + return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + + return {}; +} + +void Resource::setMetadata(std::shared_ptr&& metadata) +{ + if (status() == ResourceStatus::NO_METADATA) + setStatus(ResourceStatus::INSTALLED); + + m_metadata = metadata; +} + int Resource::compare(const Resource& other, SortType type) const { switch (type) { @@ -93,6 +125,7 @@ int Resource::compare(const Resource& other, SortType type) const QString this_name{ name() }; QString other_name{ other.name() }; + // TODO do we need this? it could result in 0 being returned removeThePrefix(this_name); removeThePrefix(other_name); @@ -118,6 +151,12 @@ int Resource::compare(const Resource& other, SortType type) const return -1; break; } + case SortType::PROVIDER: { + auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } } return 0; @@ -174,10 +213,27 @@ bool Resource::enable(EnableAction action) return true; } -bool Resource::destroy(bool attemptTrash) +auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { m_type = ResourceType::UNKNOWN; - return (attemptTrash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); + + if (!preserve_metadata) { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + destroyMetadata(index_dir); + } + + return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); +} + +auto Resource::destroyMetadata(const QDir& index_dir) -> void +{ + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_metadata = nullptr; } bool Resource::isSymLinkUnder(const QString& instPath) const @@ -204,4 +260,4 @@ auto Resource::getOriginalFileName() const -> QString if (!m_enabled) fileName.chop(9); return fileName; -} \ No newline at end of file +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index c99dab8db..87bfd4345 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -40,6 +40,7 @@ #include #include +#include "MetadataHandler.h" #include "QObjectPtr.h" enum class ResourceType { @@ -50,7 +51,14 @@ enum class ResourceType { LITEMOD, //!< The resource is a litemod }; -enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, LOADERS, MC_VERSIONS, RELEASE_TYPE }; +enum class ResourceStatus { + INSTALLED, // Both JAR and Metadata are present + NOT_INSTALLED, // Only the Metadata is present + NO_METADATA, // Only the JAR is present + UNKNOWN, // Default status +}; + +enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; @@ -75,29 +83,39 @@ class Resource : public QObject { void setFile(QFileInfo file_info); void parseFile(); - [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; } - [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } - [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } - [[nodiscard]] auto type() const -> ResourceType { return m_type; } - [[nodiscard]] bool enabled() const { return m_enabled; } - [[nodiscard]] auto getOriginalFileName() const -> QString; - [[nodiscard]] QString sizeStr() const { return m_size_str; } - [[nodiscard]] qint64 sizeInfo() const { return m_size_info; } + 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 type() const -> ResourceType { return m_type; } + bool enabled() const { return m_enabled; } + auto getOriginalFileName() const -> QString; + QString sizeStr() const { return m_size_str; } + qint64 sizeInfo() const { return m_size_info; } + + virtual auto name() const -> QString; + virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + + auto status() const -> ResourceStatus { return m_status; }; + auto metadata() -> std::shared_ptr { return m_metadata; } + auto metadata() const -> std::shared_ptr { return m_metadata; } + auto provider() const -> QString; + virtual auto homepage() const -> QString; - [[nodiscard]] virtual auto name() const -> QString { return m_name; } - [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + void setStatus(ResourceStatus status) { m_status = status; } + void setMetadata(std::shared_ptr&& metadata); + void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' */ - [[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const; + virtual int compare(Resource const& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). */ - [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const; + virtual bool applyFilter(QRegularExpression filter) const; /** Changes the enabled property, according to 'action'. * @@ -105,10 +123,10 @@ class Resource : public QObject { */ bool enable(EnableAction action); - [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } - [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } - [[nodiscard]] auto isResolved() const -> bool { return m_is_resolved; } - [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } + auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } + auto isResolving() const -> bool { return m_is_resolving; } + auto isResolved() const -> bool { return m_is_resolved; } + auto resolutionTicket() const -> int { return m_resolution_ticket; } void setResolving(bool resolving, int resolutionTicket) { @@ -117,9 +135,11 @@ class Resource : public QObject { } // Delete all files of this resource. - bool destroy(bool attemptTrash = true); + auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only. + auto destroyMetadata(const QDir& index_dir) -> void; - [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } + auto isSymLink() const -> bool { return m_file_info.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance @@ -128,9 +148,12 @@ class Resource : public QObject { * @return true * @return false */ - [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + bool isSymLinkUnder(const QString& instPath) const; - [[nodiscard]] bool isMoreThanOneHardLink() const; + 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. */ @@ -142,10 +165,16 @@ class Resource : public QObject { QString m_internal_id; /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ QString m_name; + QString m_mod_id; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; + /* Installation status of the resource. */ + ResourceStatus m_status = ResourceStatus::UNKNOWN; + + std::shared_ptr m_metadata = nullptr; + /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index b577c7272..b0082653d 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -11,20 +11,24 @@ #include #include #include +#include #include "Application.h" #include "FileSystem.h" -#include "QVariantUtils.h" -#include "StringUtils.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.h" +#include "Json.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" #include "settings/Setting.h" +#include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" -ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) - : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) +ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) { if (create_dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); @@ -48,6 +52,9 @@ ResourceFolderModel::~ResourceFolderModel() bool ResourceFolderModel::startWatching(const QStringList& paths) { + // Remove orphaned metadata next time + m_first_folder_load = true; + if (m_is_watching) return false; @@ -158,11 +165,57 @@ bool ResourceFolderModel::installResource(QString original_path) return false; } -bool ResourceFolderModel::uninstallResource(QString file_name) +void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +{ + auto install = [this, path] { installResource(std::move(path)); }; + if (vers.addonId.isValid()) { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + auto response = std::make_shared(); + auto job = FlameAPI().getProject(vers.addonId.toString(), response.get()); + 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(); + qDebug() << *response; + return; + } + try { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); + connect(&update_metadata, &Task::finished, this, install); + update_metadata.start(); + }); + + job->start(); + } else { + install(); + } +} + +bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) { for (auto& resource : m_resources) { - if (resource->fileinfo().fileName() == file_name) { - auto res = resource->destroy(false); + auto resourceFileInfo = resource->fileinfo(); + auto resourceFileName = resource->fileinfo().fileName(); + if (!resource->enabled() && resourceFileName.endsWith(".disabled")) { + resourceFileName.chop(9); + } + + if (resourceFileName == file_name) { + auto res = resource->destroy(indexDir(), preserve_metadata, false); update(); @@ -178,13 +231,11 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; for (auto i : indexes) { - if (i.column() != 0) { + if (i.column() != 0) continue; - } auto& resource = m_resources.at(i.row()); - - resource->destroy(); + resource->destroy(indexDir()); } update(); @@ -192,8 +243,36 @@ bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) return true; } +void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return; + + for (auto i : indexes) { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroyMetadata(indexDir()); + } + + update(); +} + bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(nullptr, tr("Confirm toggle"), + tr("If you enable/disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return false; + } + if (indexes.isEmpty()) return true; @@ -245,7 +324,7 @@ bool ResourceFolderModel::update() connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect( m_current_update_task.get(), &Task::finished, this, - [=] { + [this] { m_current_update_task.reset(); if (m_scheduled_update) { m_scheduled_update = false; @@ -256,7 +335,20 @@ bool ResourceFolderModel::update() }, Qt::ConnectionType::QueuedConnection); - QThreadPool::globalInstance()->start(m_current_update_task.get()); + Task::Ptr preUpdate{ createPreUpdateTask() }; + + if (preUpdate != nullptr) { + auto task = new SequentialTask("ResourceFolderModel::update"); + + task->addTask(preUpdate); + task->addTask(m_current_update_task); + + connect(task, &Task::finished, [task] { task->deleteLater(); }); + + QThreadPool::globalInstance()->start(task); + } else { + QThreadPool::globalInstance()->start(m_current_update_task.get()); + } return true; } @@ -299,20 +391,15 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) void ResourceFolderModel::onUpdateSucceeded() { - auto update_results = static_cast(m_current_update_task.get())->result(); + auto update_results = static_cast(m_current_update_task.get())->result(); auto& new_resources = update_results->resources; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto current_list = m_resources_index.keys(); QSet current_set(current_list.begin(), current_list.end()); auto new_list = new_resources.keys(); QSet new_set(new_list.begin(), new_list.end()); -#else - QSet current_set(m_resources_index.keys().toSet()); - QSet new_set(new_resources.keys().toSet()); -#endif applyUpdates(current_set, new_set, new_resources); } @@ -329,7 +416,11 @@ void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) Task* ResourceFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir); + auto index_dir = indexDir(); + auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, + [this](const QFileInfo& file) { return createResource(file); }); + m_first_folder_load = false; + return task; } bool ResourceFolderModel::hasPendingParseTasks() const @@ -419,6 +510,8 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: + return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: @@ -442,17 +535,14 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); return {}; } case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return {}; - } + if (column == ActiveColumn) + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return {}; default: return {}; } @@ -465,17 +555,6 @@ bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] con return false; if (role == Qt::CheckStateRole) { - if (m_instance != nullptr && m_instance->isRunning()) { - auto response = - CustomMessageBox::selectable(nullptr, tr("Confirm toggle"), - tr("If you enable/disable this resource while the game is running it may crash your game.\n" - "Are you sure you want to do this?"), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - - if (response != QMessageBox::Yes) - return false; - } return setResourceEnabled({ index }, EnableAction::TOGGLE); } @@ -490,22 +569,23 @@ QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orien case ActiveColumn: case NameColumn: case DateColumn: + case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. switch (section) { case ActiveColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("Is the resource enabled?"); case NameColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The name of the resource."); case DateColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The date and time this resource was last changed (or added)."); + case ProviderColumn: + return tr("The source provider of the resource."); case SizeColumn: return tr("The size of the resource."); default: @@ -528,30 +608,89 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { - auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) - : m_instance->settings()->registerSetting(setting_name); + auto 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()); + + auto stateSetting = m_instance->settings()->getSetting(stateSettingName); + stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); - setting->set(tree->header()->saveState()); + // 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(); + 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) { + if (m_columnsHideable[i]) { + auto name = m_column_names[i]; + visibility[name] = !tree->isColumnHidden(i); + } + } + settings->set(visibilitySettingName, Json::fromMap(visibility)); } void ResourceFolderModel::loadColumns(QTreeView* tree) { - for (auto i = 0; i < m_columnsHiddenByDefault.size(); ++i) { - tree->setColumnHidden(i, m_columnsHiddenByDefault[i]); - } + 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()); - auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) - : m_instance->settings()->registerSetting(setting_name); + auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); + tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); - tree->header()->restoreState(setting->get().toByteArray()); + auto setVisible = [this, tree](QVariant value) { + auto visibility = Json::toMap(value.toString()); + for (auto i = 0; i < m_column_names.size(); ++i) { + if (m_columnsHideable[i]) { + auto name = m_column_names[i]; + tree->setColumnHidden(i, !visibility.value(name, false).toBool()); + } + } + }; + + auto const defaultValue = Json::fromMap({ + { "Image", true }, + { "Version", true }, + { "Last Modified", true }, + { "Provider", true }, + { "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(); + if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { + settings = APPLICATION->settings(); + } + auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); + setVisible(visibility->get()); + + // 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) { + if (!m_instance->settings()->get(overrideSettingName).toBool()) { + setVisible(value); + } + }); } QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* 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()); + + act->setCheckable(true); + act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); + + connect(act, &QAction::toggled, tree, [this, tree, overrideSettingName](bool toggled) { + m_instance->settings()->set(overrideSettingName, toggled); + saveColumns(tree); + }); + + menu->addAction(act); + } menu->addSeparator()->setText(tr("Show / Hide Columns")); for (int col = 0; col < columnCount(); ++col) { @@ -591,8 +730,7 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const } /* Standard Proxy Model for createFilterProxyModel */ -[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, - [[maybe_unused]] const QModelIndex& source_parent) const +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); if (!model) @@ -603,7 +741,7 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const return resource.applyFilter(filterRegularExpression()); } -[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { auto* model = qobject_cast(sourceModel()); if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { @@ -651,3 +789,126 @@ void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) } endRemoveRows(); } + +void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) +{ + // see if the kept resources changed in some way + { + QSet kept_set = current_set; + kept_set.intersect(new_set); + + for (auto const& kept : kept_set) { + auto row_it = m_resources_index.constFind(kept); + Q_ASSERT(row_it != m_resources_index.constEnd()); + auto row = row_it.value(); + + auto& new_resource = new_resources[kept]; + auto const& current_resource = m_resources.at(row); + + if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { + // no significant change, ignore... + continue; + } + + // If the resource is resolving, but something about it changed, we don't want to + // continue the resolving. + if (current_resource->isResolving()) { + auto ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + m_resources[row].reset(new_resource); + resolveResource(m_resources.at(row)); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet removed_set = current_set; + removed_set.subtract(new_set); + + QList removed_rows; + for (auto& removed : removed_set) + removed_rows.append(m_resources_index[removed]); + + std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); + + for (auto& removed_index : removed_rows) { + auto removed_it = m_resources.begin() + removed_index; + + Q_ASSERT(removed_it != m_resources.end()); + + if ((*removed_it)->isResolving()) { + auto ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet added_set = new_set; + added_set.subtract(current_set); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added_set.size() > 0) { + beginInsertRows(QModelIndex(), static_cast(m_resources.size()), + static_cast(m_resources.size() + added_set.size() - 1)); + + for (auto& added : added_set) { + auto res = new_resources[added]; + m_resources.append(res); + resolveResource(m_resources.last()); + } + + endInsertRows(); + } + } + + // update index + { + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + } +} +Resource::Ptr ResourceFolderModel::find(QString id) +{ + auto iter = + std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); + if (iter == m_resources.constEnd()) + return nullptr; + return *iter; +} +QList ResourceFolderModel::allResources() +{ + QList result; + result.reserve(m_resources.size()); + for (const Resource ::Ptr& resource : m_resources) + result.append((resource.get())); + return result; +} +QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) +{ + QList result; + for (const QModelIndex& index : indexes) { + if (index.column() != 0) + continue; + result.append(&at(index.row())); + } + return result; +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 354eedc28..0526b5bbf 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -19,6 +19,38 @@ class QSortFilterProxyModel; +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + T& at(int index) \ + { \ + return *static_cast(m_resources[index].get()); \ + } \ + const T& at(int index) const \ + { \ + return *static_cast(m_resources.at(index).get()); \ + } \ + QList selected##T##s(const QModelIndexList& indexes) \ + { \ + QList result; \ + for (const QModelIndex& index : indexes) { \ + if (index.column() != 0) \ + continue; \ + \ + result.append(&at(index.row())); \ + } \ + return result; \ + } \ + QList all##T##s() \ + { \ + QList result; \ + result.reserve(m_resources.size()); \ + \ + for (const Resource::Ptr& resource : m_resources) \ + result.append(static_cast(resource.get())); \ + \ + return result; \ + } + /** A basic model for external resources. * * This model manages a list of resources. As such, external users of such resources do not own them, @@ -29,7 +61,7 @@ class QSortFilterProxyModel; class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: - ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true); + ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ~ResourceFolderModel() override; virtual QString id() const { return "resource"; } @@ -49,8 +81,10 @@ class ResourceFolderModel : public QAbstractListModel { bool stopWatching(const QStringList& paths); /* Helper methods for subclasses, using a predetermined list of paths. */ - virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); } - virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); } + virtual bool startWatching() { return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } + virtual bool stopWatching() { return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } + + virtual QDir indexDir() const { return { QString("%1/.index").arg(dir().absolutePath()) }; } /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. @@ -59,12 +93,15 @@ class ResourceFolderModel : public QAbstractListModel { */ virtual bool installResource(QString path); + virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); + /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ - virtual bool uninstallResource(QString file_name); + virtual bool uninstallResource(const QString& file_name, bool preserve_metadata = false); virtual bool deleteResources(const QModelIndexList&); + virtual void deleteMetadata(const QModelIndexList&); /** Applies the given 'action' to the resources in 'indexes'. * @@ -78,43 +115,48 @@ class ResourceFolderModel : public QAbstractListModel { /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource::Ptr res); - [[nodiscard]] qsizetype size() const { return m_resources.size(); } + qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } - [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); } - [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } - [[nodiscard]] QList const& all() const { return m_resources; } - [[nodiscard]] QDir const& dir() const { return m_dir; } + Resource& at(int index) { return *m_resources[index].get(); } + const Resource& at(int index) const { return *m_resources.at(index).get(); } + QList selectedResources(const QModelIndexList& indexes); + QList allResources(); + + Resource::Ptr find(QString id); + + QDir const& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having * such tasks would introduce an undefined behavior, most likely resulting in a crash. */ - [[nodiscard]] bool hasPendingParseTasks() const; + bool hasPendingParseTasks() const; /* Qt behavior */ /* Basic columns */ - enum Columns { ActiveColumn = 0, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; + QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } - [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } - [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } + 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; } - [[nodiscard]] Qt::DropActions supportedDropActions() const override; + Qt::DropActions supportedDropActions() const override; /// flags, mostly to support drag&drop - [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override; - [[nodiscard]] QStringList mimeTypes() const override; - bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + [[nodiscard]] bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; [[nodiscard]] bool validateIndex(const QModelIndex& index) const; - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void setupHeaderAction(QAction* act, int column); void saveColumns(QTreeView* tree); @@ -127,16 +169,16 @@ class ResourceFolderModel : public QAbstractListModel { */ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); - [[nodiscard]] SortType columnToSortKey(size_t column) const; - [[nodiscard]] QList columnResizeModes() const { return m_column_resize_modes; } + SortType columnToSortKey(size_t column) const; + QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: - [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; - [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; QString instDirPath() const; @@ -146,6 +188,7 @@ class ResourceFolderModel : public QAbstractListModel { void parseFinished(); protected: + [[nodiscard]] virtual Task* createPreUpdateTask() { return nullptr; } /** This creates a new update task to be executed by update(). * * The task should load and parse all resources necessary, and provide a way of accessing such results. @@ -153,7 +196,9 @@ class ResourceFolderModel : public QAbstractListModel { * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. * If such work is needed, try using it in the Task create by createParseTask() instead! */ - [[nodiscard]] virtual Task* createUpdateTask(); + [[nodiscard]] Task* createUpdateTask(); + + [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) { return new Resource(info); } /** This creates a new parse task to be executed by onUpdateSucceeded(). * @@ -167,10 +212,8 @@ class ResourceFolderModel : public QAbstractListModel { * It uses set operations to find differences between the current state and the updated state, * to act only on those disparities. * - * The implementation is at the end of this header. */ - template - void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); + void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); protected slots: void directoryChanged(QString); @@ -195,19 +238,21 @@ class ResourceFolderModel : public QAbstractListModel { protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! - QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::SIZE }; - QStringList m_column_names = { "Enable", "Name", "Last Modified", "Size" }; - QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Size") }; + QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - QList m_columnsHideable = { false, false, true, true }; - QList m_columnsHiddenByDefault = { false, false, false, false }; + QHeaderView::Interactive, QHeaderView::Interactive }; + QList m_columnsHideable = { false, false, true, true, true }; QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; + bool m_is_indexed; + bool m_first_folder_load = true; + Task::Ptr m_current_update_task = nullptr; bool m_scheduled_update = false; @@ -220,133 +265,3 @@ class ResourceFolderModel : public QAbstractListModel { QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; - -/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ -#define RESOURCE_HELPERS(T) \ - [[nodiscard]] T* operator[](int index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] T* at(int index) \ - { \ - return static_cast(m_resources[index].get()); \ - } \ - [[nodiscard]] const T* at(int index) const \ - { \ - return static_cast(m_resources.at(index).get()); \ - } \ - [[nodiscard]] T* first() \ - { \ - return static_cast(m_resources.first().get()); \ - } \ - [[nodiscard]] T* last() \ - { \ - return static_cast(m_resources.last().get()); \ - } \ - [[nodiscard]] T* find(QString id) \ - { \ - auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \ - [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \ - if (iter == m_resources.constEnd()) \ - return nullptr; \ - return static_cast((*iter).get()); \ - } - -/* Template definition to avoid some code duplication */ -template -void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) -{ - // see if the kept resources changed in some way - { - QSet kept_set = current_set; - kept_set.intersect(new_set); - - for (auto const& kept : kept_set) { - auto row_it = m_resources_index.constFind(kept); - Q_ASSERT(row_it != m_resources_index.constEnd()); - auto row = row_it.value(); - - auto& new_resource = new_resources[kept]; - auto const& current_resource = m_resources.at(row); - - if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { - // no significant change, ignore... - continue; - } - - // If the resource is resolving, but something about it changed, we don't want to - // continue the resolving. - if (current_resource->isResolving()) { - auto ticket = current_resource->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); - task->abort(); - } - } - - m_resources[row].reset(new_resource); - resolveResource(m_resources.at(row)); - emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); - } - } - - // remove resources no longer present - { - QSet removed_set = current_set; - removed_set.subtract(new_set); - - QList removed_rows; - for (auto& removed : removed_set) - removed_rows.append(m_resources_index[removed]); - - std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); - - for (auto& removed_index : removed_rows) { - auto removed_it = m_resources.begin() + removed_index; - - Q_ASSERT(removed_it != m_resources.end()); - - if ((*removed_it)->isResolving()) { - auto ticket = (*removed_it)->resolutionTicket(); - if (m_active_parse_tasks.contains(ticket)) { - auto task = (*m_active_parse_tasks.find(ticket)).get(); - task->abort(); - } - } - - beginRemoveRows(QModelIndex(), removed_index, removed_index); - m_resources.erase(removed_it); - endRemoveRows(); - } - } - - // add new resources to the end - { - QSet added_set = new_set; - added_set.subtract(current_set); - - // When you have a Qt build with assertions turned on, proceeding here will abort the application - if (added_set.size() > 0) { - beginInsertRows(QModelIndex(), static_cast(m_resources.size()), - static_cast(m_resources.size() + added_set.size() - 1)); - - for (auto& added : added_set) { - auto res = new_resources[added]; - m_resources.append(res); - resolveResource(m_resources.last()); - } - - endInsertRows(); - } - } - - // update index - { - m_resources_index.clear(); - int idx = 0; - for (auto const& mod : qAsConst(m_resources)) { - m_resources_index[mod->internal_id()] = idx; - idx++; - } - } -} diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 81fb91485..ccf52e8e4 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -3,13 +3,9 @@ #include #include #include -#include - #include "MTPixmapCache.h" #include "Version.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" - // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats static const QMap> s_pack_format_versions = { @@ -31,69 +27,6 @@ static const QMap> s_pack_format_versions = { { 34, { Version("24w21a"), Version("1.21") } } }; -void ResourcePack::setPackFormat(int new_format_id) -{ - QMutexLocker locker(&m_data_lock); - - if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '" << new_format_id << "' is not a recognized resource pack id!"; - } - - m_pack_format = new_format_id; -} - -void ResourcePack::setDescription(QString new_description) -{ - QMutexLocker locker(&m_data_lock); - - m_description = new_description; -} - -void ResourcePack::setImage(QImage new_image) const -{ - QMutexLocker locker(&m_data_lock); - - Q_ASSERT(!new_image.isNull()); - - if (m_pack_image_cache_key.key.isValid()) - PixmapCache::instance().remove(m_pack_image_cache_key.key); - - // scale the image to avoid flooding the pixmapcache - auto pixmap = - QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - - m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); - m_pack_image_cache_key.was_ever_used = true; - - // This can happen if the pixmap is too big to fit in the cache :c - if (!m_pack_image_cache_key.key.isValid()) { - qWarning() << "Could not insert a image cache entry! Ignoring it."; - m_pack_image_cache_key.was_ever_used = false; - } -} - -QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const -{ - QPixmap cached_image; - if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { - if (size.isNull()) - return cached_image; - return cached_image.scaled(size, mode, Qt::SmoothTransformation); - } - - // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) { - return {}; - } else { - qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading..."; - PixmapCache::markCacheMissByEviciton(); - } - - // Imaged got evicted from the cache. Re-process it and retry. - ResourcePackUtils::processPackPNG(*this); - return image(size); -} - std::pair ResourcePack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { @@ -102,44 +35,3 @@ std::pair ResourcePack::compatibleVersions() const return s_pack_format_versions.constFind(m_pack_format).value(); } - -int ResourcePack::compare(const Resource& other, SortType type) const -{ - auto const& cast_other = static_cast(other); - switch (type) { - default: - return Resource::compare(other, type); - case SortType::PACK_FORMAT: { - auto this_ver = packFormat(); - auto other_ver = cast_other.packFormat(); - - if (this_ver > other_ver) - return 1; - if (this_ver < other_ver) - return -1; - break; - } - } - return 0; -} - -bool ResourcePack::applyFilter(QRegularExpression filter) const -{ - if (filter.match(description()).hasMatch()) - return true; - - if (filter.match(QString::number(packFormat())).hasMatch()) - return true; - - if (filter.match(compatibleVersions().first.toString()).hasMatch()) - return true; - if (filter.match(compatibleVersions().second.toString()).hasMatch()) - return true; - - return Resource::applyFilter(filter); -} - -bool ResourcePack::valid() const -{ - return m_pack_format != 0; -} diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 2254fc5c4..9345e9c27 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -1,6 +1,7 @@ #pragma once #include "Resource.h" +#include "minecraft/mod/DataPack.h" #include #include @@ -14,58 +15,12 @@ class Version; * Store localized descriptions * */ -class ResourcePack : public Resource { +class ResourcePack : public DataPack { Q_OBJECT public: - using Ptr = shared_qobject_ptr; + ResourcePack(QObject* parent = nullptr) : DataPack(parent) {} + ResourcePack(QFileInfo file_info) : DataPack(file_info) {} - ResourcePack(QObject* parent = nullptr) : Resource(parent) {} - ResourcePack(QFileInfo file_info) : Resource(file_info) {} - - /** Gets the numerical ID of the pack format. */ - [[nodiscard]] int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] std::pair compatibleVersions() const; - - /** Gets the description of the resource pack. */ - [[nodiscard]] QString description() const { return m_description; } - - /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; - - /** Thread-safe. */ - void setPackFormat(int new_format_id); - - /** Thread-safe. */ - void setDescription(QString new_description); - - /** Thread-safe. */ - void setImage(QImage new_image) const; - - bool valid() const override; - - [[nodiscard]] int compare(Resource const& other, SortType type) const override; - [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; - - protected: - mutable QMutex m_data_lock; - - /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta - */ - int m_pack_format = 0; - - /** The resource pack's description, as defined in the pack.mcmeta file. - */ - QString m_description; - - /** The resource pack's image file cache key, for access in the QPixmapCache global instance. - * - * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), - * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. - */ - struct { - QPixmapCache::Key key; - bool was_ever_used = false; - } mutable m_pack_image_cache_key; + std::pair compatibleVersions() const override; }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 1aa3a2e8c..5680f4c2d 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -35,28 +35,25 @@ */ #include "ResourcePackFolderModel.h" -#include -#include #include #include -#include "Application.h" #include "Version.h" -#include "minecraft/mod/Resource.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) +ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) { - m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Size" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true, true }; - m_columnsHiddenByDefault = { false, false, false, false, false, false }; + m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -73,12 +70,12 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const case NameColumn: return m_resources[row]->name(); case PackFormatColumn: { - auto resource = at(row); - auto pack_format = resource->packFormat(); + auto& resource = at(row); + auto pack_format = resource.packFormat(); if (pack_format == 0) return tr("Unrecognized"); - auto version_bounds = resource->compatibleVersions(); + auto version_bounds = resource.compatibleVersions(); if (version_bounds.first.toString().isEmpty()) return QString::number(pack_format); @@ -87,17 +84,18 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: + return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); - default: return {}; } case Qt::DecorationRole: { - if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + 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 at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -107,14 +105,14 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } if (column == NameColumn) { - if (at(row)->isSymLinkUnder(instDirPath())) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); ; } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } @@ -127,12 +125,9 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } return {}; case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; - default: - return {}; - } + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + return {}; default: return {}; } @@ -148,6 +143,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case PackFormatColumn: case DateColumn: case ImageColumn: + case ProviderColumn: case SizeColumn: return columnNames().at(section); default: @@ -157,7 +153,7 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O case Qt::ToolTipRole: switch (section) { case ActiveColumn: - return tr("Is the resource pack enabled? (Only valid for ZIPs)"); + return tr("Is the resource pack enabled?"); case NameColumn: return tr("The name of the resource pack."); case PackFormatColumn: @@ -165,6 +161,8 @@ QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::O return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); + case ProviderColumn: + return tr("The source provider of the resource pack."); case SizeColumn: return tr("The size of the resource pack."); default: @@ -185,12 +183,7 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const return parent.isValid() ? 0 : NUM_COLUMNS; } -Task* ResourcePackFolderModel::createUpdateTask() -{ - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); -} - Task* ResourcePackFolderModel::createParseTask(Resource& resource) { - return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast(resource)); + return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); } diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 755b9c4c6..b552c324e 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -7,18 +7,18 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, SizeColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; - explicit ResourcePackFolderModel(const QString& dir, BaseInstance* instance); + explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); - virtual QString id() const override { return "resourcepacks"; } + QString id() const override { return "resourcepacks"; } - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(ResourcePack) diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index ccb344cb5..99e51fcae 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -22,8 +22,6 @@ #include "ShaderPack.h" -#include - void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&m_data_lock); diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index ec0f9404e..9275ebed8 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -45,7 +45,7 @@ class ShaderPack : public Resource { public: using Ptr = shared_qobject_ptr; - [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; } + ShaderPackFormat packFormat() const { return m_pack_format; } ShaderPack(QObject* parent = nullptr) : Resource(parent) {} ShaderPack(QFileInfo file_info) : Resource(file_info) {} diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.cpp b/launcher/minecraft/mod/ShaderPackFolderModel.cpp new file mode 100644 index 000000000..ea68e0257 --- /dev/null +++ b/launcher/minecraft/mod/ShaderPackFolderModel.cpp @@ -0,0 +1,56 @@ +#include "ShaderPackFolderModel.h" +#include "FileSystem.h" + +namespace { +class ShaderPackIndexMigrateTask : public Task { + Q_OBJECT + public: + ShaderPackIndexMigrateTask(QDir resourceDir, QDir indexDir) : m_resourceDir(std::move(resourceDir)), m_indexDir(std::move(indexDir)) {} + + void executeTask() override + { + if (!m_indexDir.exists()) { + qDebug() << m_indexDir.absolutePath() << "does not exist; nothing to migrate"; + emitSucceeded(); + return; + } + + QStringList pwFiles = m_indexDir.entryList({ "*.pw.toml" }, QDir::Files); + bool movedAll = true; + + for (const auto& file : pwFiles) { + QString src = m_indexDir.filePath(file); + QString dest = m_resourceDir.filePath(file); + + if (FS::move(src, dest)) { + qDebug() << "Moved" << src << "to" << dest; + } else { + movedAll = false; + } + } + + if (!movedAll) { + // FIXME: not shown in the UI + emitFailed(tr("Failed to migrate shaderpack metadata from .index")); + return; + } + + if (!FS::deletePath(m_indexDir.absolutePath())) { + emitFailed(tr("Failed to remove old .index dir")); + return; + } + + emitSucceeded(); + } + + private: + QDir m_resourceDir, m_indexDir; +}; +} // namespace + +Task* ShaderPackFolderModel::createPreUpdateTask() +{ + return new ShaderPackIndexMigrateTask(m_dir, ResourceFolderModel::indexDir()); +} + +#include "ShaderPackFolderModel.moc" diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index 186d02139..9b0180180 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -2,24 +2,35 @@ #include "ResourceFolderModel.h" #include "minecraft/mod/ShaderPack.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: - explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} + explicit ShaderPackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) + {} virtual QString id() const override { return "shaderpacks"; } - [[nodiscard]] Task* createUpdateTask() override - { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); - } + [[nodiscard]] Resource* createResource(const QFileInfo& info) override { return new ShaderPack(info); } [[nodiscard]] Task* createParseTask(Resource& resource) override { return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); } + + QDir indexDir() const override { return m_dir; } + + Task* createPreUpdateTask() override; + + // avoid watching twice + virtual bool startWatching() override { return ResourceFolderModel::startWatching({ m_dir.absolutePath() }); } + virtual bool stopWatching() override { return ResourceFolderModel::stopWatching({ m_dir.absolutePath() }); } + + RESOURCE_HELPERS(ShaderPack); + + private: + QMutex m_migrateLock; }; diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 04cc36310..a1ef7f525 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -21,8 +21,6 @@ #include #include -#include - #include "MTPixmapCache.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h index bf4b5b6b4..1327e2f61 100644 --- a/launcher/minecraft/mod/TexturePack.h +++ b/launcher/minecraft/mod/TexturePack.h @@ -37,10 +37,10 @@ class TexturePack : public Resource { TexturePack(QFileInfo file_info) : Resource(file_info) {} /** Gets the description of the texture pack. */ - [[nodiscard]] QString description() const { return m_description; } + QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 48e940e9b..57c5f8ee9 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -33,28 +33,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include - -#include "Application.h" - #include "TexturePackFolderModel.h" -#include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.h" -TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) -{ - m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Size" }); - m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Size") }); - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, - QHeaderView::Interactive }; - m_columnsHideable = { false, true, false, true, true }; -} - -Task* TexturePackFolderModel::createUpdateTask() +TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) @@ -77,6 +69,8 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: + return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: @@ -84,14 +78,14 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const } case Qt::ToolTipRole: if (column == NameColumn) { - if (at(row)->isSymLinkUnder(instDirPath())) { + if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") - .arg(at(row)->fileinfo().canonicalFilePath()); + .arg(at(row).fileinfo().canonicalFilePath()); ; } - if (at(row)->isMoreThanOneHardLink()) { + if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } @@ -99,10 +93,10 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { - if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + 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 at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } @@ -130,6 +124,7 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case NameColumn: case DateColumn: case ImageColumn: + case ProviderColumn: case SizeColumn: return columnNames().at(section); default: @@ -138,14 +133,13 @@ QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Or case Qt::ToolTipRole: { switch (section) { case ActiveColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("Is the texture pack enabled?"); case NameColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The name of the texture pack."); case DateColumn: - //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. return tr("The date and time this texture pack was last changed (or added)."); + case ProviderColumn: + return tr("The source provider of the texture pack."); case SizeColumn: return tr("The size of the texture pack."); default: diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index de90f879f..37f78d8d7 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -44,19 +44,18 @@ class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: - enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, SizeColumn, NUM_COLUMNS }; + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; - explicit TexturePackFolderModel(const QString& dir, std::shared_ptr instance); + explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "texturepacks"; } - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; - explicit TexturePackFolderModel(const QString& dir, BaseInstance* instance); - [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(TexturePack) diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h index 5985fc8ad..702a3edf6 100644 --- a/launcher/minecraft/mod/WorldSave.h +++ b/launcher/minecraft/mod/WorldSave.h @@ -38,9 +38,9 @@ class WorldSave : public Resource { WorldSave(QFileInfo file_info) : Resource(file_info) {} /** Gets the format of the save. */ - [[nodiscard]] WorldSaveFormat saveFormat() const { return m_save_format; } + WorldSaveFormat saveFormat() const { return m_save_format; } /** Gets the name of the save dir (first found in multi mode). */ - [[nodiscard]] QString saveDirName() const { return m_save_dir_name; } + QString saveDirName() const { return m_save_dir_name; } /** Thread-safe. */ void setSaveFormat(WorldSaveFormat new_save_format); diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h deleted file mode 100644 index 168e4b6fa..000000000 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "Application.h" -#include "FileSystem.h" -#include "minecraft/mod/Resource.h" - -#include "tasks/Task.h" - -/** Very simple task that just loads a folder's contents directly. - */ -class BasicFolderLoadTask : public Task { - Q_OBJECT - public: - struct Result { - QMap resources; - }; - using ResultPtr = std::shared_ptr; - - [[nodiscard]] ResultPtr result() const { return m_result; } - - public: - BasicFolderLoadTask(QDir dir) : Task(false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) - { - m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { return makeShared(entry); }; - } - BasicFolderLoadTask(QDir dir, std::function create_function) - : Task(false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread()) - {} - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override - { - m_aborted.store(true); - return true; - } - - void executeTask() override - { - if (thread() != m_thread_to_spawn_into) - connect(this, &Task::finished, this->thread(), &QThread::quit); - - m_dir.refresh(); - for (auto entry : m_dir.entryInfoList()) { - auto filePath = entry.absoluteFilePath(); - if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { - continue; - } - auto newFilePath = FS::getUniqueResourceName(filePath); - if (newFilePath != filePath) { - FS::move(filePath, newFilePath); - entry = QFileInfo(newFilePath); - } - auto resource = m_create_func(entry); - resource->moveToThread(m_thread_to_spawn_into); - m_result->resources.insert(resource->internal_id(), resource); - } - - if (m_aborted) - emit finished(); - else - emitSucceeded(); - } - - private: - QDir m_dir; - ResultPtr m_result; - - std::atomic m_aborted = false; - - std::function m_create_func; - - /** This is the thread in which we should put new mod objects */ - QThread* m_thread_to_spawn_into; -}; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index b63d36361..668209930 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -27,12 +27,8 @@ #include "minecraft/mod/MetadataHandler.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" -#include "modplatform/flame/FlameAPI.h" -#include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/flame/FlameResourceModels.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" static Version mcVersion(BaseInstance* inst) { @@ -55,14 +51,7 @@ static bool checkDependencies(std::shared_ptr> selected) - : SequentialTask(tr("Get dependencies")) - , m_selected(selected) - , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), - std::make_shared() } - , m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared(*instance), - std::make_shared() } - , m_version(mcVersion(instance)) - , m_loaderType(mcLoaders(instance)) + : SequentialTask(tr("Get dependencies")), m_selected(selected), m_version(mcVersion(instance)), m_loaderType(mcLoaders(instance)) { for (auto mod : folder->allMods()) { m_mods_file_names << mod->fileinfo().fileName(); @@ -87,7 +76,7 @@ ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::D { if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { + 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()) { @@ -144,10 +133,10 @@ QList GetModDependenciesTask::getDependenciesForVersion Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { - auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + auto provider = pDep->pack->provider; auto responseInfo = std::make_shared(); - auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo); - QObject::connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { + auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo.get()); + connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -158,9 +147,10 @@ Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptrloadIndexedPack(*pDep->pack, obj); + auto obj = provider == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + + getAPI(provider)->loadIndexedPack(*pDep->pack, obj); } catch (const JSONValidationError& e) { removePack(pDep->pack->addonId); qDebug() << doc; @@ -181,7 +171,8 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen pDep->pack->provider = providerName; m_pack_dependencies.append(pDep); - auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + + auto provider = providerName; auto tasks = makeShared( QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); @@ -191,45 +182,30 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; - ResourceAPI::DependencySearchCallbacks callbacks; + ResourceAPI::Callback callbacks; callbacks.on_fail = [](QString reason, int) { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; - callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { - try { - QJsonArray arr; - if (dep.version.length() != 0 && doc.isObject()) { - arr.append(doc.object()); - } else { - arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - } - pDep->version = provider.mod->loadDependencyVersions(dep, arr); - if (!pDep->version.addonId.isValid()) { - 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.name && dep.addonId == o.quilt; }); - if (over != overide.cend()) { - removePack(dep.addonId); - addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level)); - return; - } + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) { + pDep->version = pack; + if (!pDep->version.addonId.isValid()) { + 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; }); + if (over != overide.cend()) { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); + return; } - removePack(dep.addonId); - qWarning() << "Error while reading mod version empty "; - qDebug() << doc; - return; } - pDep->version.is_currently_selected = true; - pDep->pack->versions = { pDep->version }; - pDep->pack->versionsLoaded = true; - - } catch (const JSONValidationError& e) { removePack(dep.addonId); - qDebug() << doc; - qWarning() << "Error while reading mod version: " << e.cause(); return; } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + if (level == 0) { removePack(dep.addonId); qWarning() << "Dependency cycle exceeded"; @@ -237,10 +213,10 @@ 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.name); + auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); - addTask(prepareDependencyTask(dep_, provider.name, level)); + addTask(prepareDependencyTask(dep_, provider, level)); } else { addTask(getProjectInfoTask(pDep)); } @@ -249,12 +225,12 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen removePack(pDep->version.addonId); return; } - for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { - addTask(prepareDependencyTask(dep_, provider.name, level - 1)); + for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) { + addTask(prepareDependencyTask(dep_, provider, level - 1)); } }; - auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks)); + auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); tasks->addTask(version); return tasks; } diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 29c77f9fe..3530d6cc0 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -19,7 +19,6 @@ #pragma once #include -#include #include #include #include @@ -29,6 +28,8 @@ #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/pages/modplatform/ModModel.h" @@ -55,17 +56,20 @@ class GetModDependenciesTask : public SequentialTask { QStringList required_by; }; - struct Provider { - ModPlatform::ResourceProvider name; - std::shared_ptr mod; - std::shared_ptr api; - }; - explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); + private: + inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) + { + if (provider == ModPlatform::ResourceProvider::FLAME) + return &m_flameAPI; + else + return &m_modrinthAPI; + } + protected slots: Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, @@ -83,9 +87,10 @@ class GetModDependenciesTask : public SequentialTask { QList> m_mods; QList> m_selected; QStringList m_mods_file_names; - Provider m_flame_provider; - Provider m_modrinth_provider; Version m_version; ModPlatform::ModLoaderTypes m_loaderType; + + ModrinthAPI m_modrinthAPI; + FlameAPI m_flameAPI; }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 19b15709d..60f836b4d 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -23,18 +23,16 @@ #include "FileSystem.h" #include "Json.h" - -#include -#include -#include +#include "archive/ArchiveReader.h" +#include "minecraft/mod/ResourcePack.h" #include namespace DataPackUtils { -bool process(DataPack& pack, ProcessingLevel level) +bool process(DataPack* pack, ProcessingLevel level) { - switch (pack.type()) { + switch (pack->type()) { case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: @@ -45,16 +43,16 @@ bool process(DataPack& pack, ProcessingLevel level) } } -bool processFolder(DataPack& pack, ProcessingLevel level) +bool processFolder(DataPack* pack, ProcessingLevel level) { - Q_ASSERT(pack.type() == ResourceType::FOLDER); + Q_ASSERT(pack->type() == ResourceType::FOLDER); auto mcmeta_invalid = [&pack]() { - qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; - QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) @@ -72,76 +70,115 @@ bool processFolder(DataPack& pack, ProcessingLevel level) return mcmeta_invalid(); // mcmeta file isn't a valid file } - QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); - if (!data_dir_info.exists() || !data_dir_info.isDir()) { - return false; // data dir does not exists or isn't valid - } - if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } - - return true; // all tests passed -} - -bool processZIP(DataPack& pack, ProcessingLevel level) -{ - Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - - auto mcmeta_invalid = [&pack]() { - qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; - return false; // the mcmeta is not optional + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional }; - if (zip.setCurrentFile("pack.mcmeta")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return mcmeta_invalid(); - } + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file - auto data = file.readAll(); + auto data = pack_png_file.readAll(); - bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); - file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid } } else { - return mcmeta_invalid(); // could not set pack.mcmeta as current file. + return png_invalid(); // pack.png does not exists or is not a valid file. } - QuaZipDir zipDir(&zip); - if (!zipDir.exists("/data")) { - return false; // data dir does not exists at zip root + return true; // all tests passed +} + +bool processZIP(DataPack* pack, ProcessingLevel level) +{ + Q_ASSERT(pack->type() == ResourceType::ZIPFILE); + + MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); + + bool metaParsed = false; + bool iconParsed = false; + bool mcmeta_result = false; + bool pack_png_result = false; + if (!zip.parse( + [&metaParsed, &iconParsed, &mcmeta_result, &pack_png_result, pack, level](MMCZip::ArchiveReader::File* f, bool& breakControl) { + bool skip = true; + if (!metaParsed && f->filename() == "pack.mcmeta") { + metaParsed = true; + skip = false; + auto data = f->readAll(); + + mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + if (!mcmeta_result) { + breakControl = true; + return true; // mcmeta invalid + } + } + if (!iconParsed && level != ProcessingLevel::BasicInfoOnly && f->filename() == "pack.png") { + iconParsed = true; + skip = false; + auto data = f->readAll(); + + pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + if (!pack_png_result) { + breakControl = true; + return true; // pack.png invalid + } + } + if (skip) { + f->skip(); + } + if (metaParsed && (level == ProcessingLevel::BasicInfoOnly || iconParsed)) { + breakControl = true; + } + + return true; + })) { + return false; // can't open zip file + } + if (!mcmeta_result) { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional } if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } - zip.close(); + if (!pack_png_result) { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + } return true; } // https://minecraft.wiki/w/Data_pack#pack.mcmeta -bool processMCMeta(DataPack& pack, QByteArray&& raw_data) +// https://minecraft.wiki/w/Raw_JSON_text_format +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +bool processMCMeta(DataPack* pack, QByteArray&& raw_data) { + QJsonParseError parse_error; + auto json_doc = Json::parseUntilGarbage(raw_data, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse JSON:" << parse_error.errorString(); + return false; + } + try { - auto json_doc = QJsonDocument::fromJson(raw_data); auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); - - pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - pack.setDescription(Json::ensureString(pack_obj, "description", "")); + pack->setPackFormat(pack_obj["pack_format"].toInt()); + pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); return false; @@ -149,29 +186,170 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data) return true; } -bool validate(QFileInfo file) +QString buildStyle(const QJsonObject& obj) { - DataPack dp{ file }; - return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); + QStringList styles; + if (auto color = obj["color"].toString(); !color.isEmpty()) { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) { + QString weight = "normal"; + if (obj["bold"].toBool()) { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) { + QString style = "normal"; + if (obj["italic"].toBool()) { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); } -} // namespace DataPackUtils +QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) +{ + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; +} + +QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) +{ + underline = obj["underlined"].toBool(underline); + strikethrough = obj["strikethrough"].toBool(strikethrough); -LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(false), m_token(token), m_data_pack(dp) {} + QString result = obj["text"].toString(); + if (underline) { + result = QString("%1").arg(result); + } + if (strikethrough) { + result = QString("%1").arg(result); + } + // the extra needs to be a array + result += processComponent(obj["extra"].toArray(), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) { + result = QString("%2").arg(style, result); + } + if (obj.contains("clickEvent")) { + auto click_event = obj["clickEvent"].toObject(); + auto action = click_event["action"].toString(); + auto value = click_event["value"].toString(); + if (action == "open_url" && !value.isEmpty()) { + result = QString("%2").arg(value, result); + } + } + return result; +} -bool LocalDataPackParseTask::abort() +QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) { - m_aborted = true; + if (value.isString()) { + return value.toString(); + } + if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) { + return QString::number(value.toDouble()); + } + if (value.isArray()) { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; +} + +bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack->setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + return false; + } return true; } +bool processPackPNG(const DataPack* pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack->type()) { + case ResourceType::FOLDER: { + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + case ResourceType::ZIPFILE: { + MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); + auto f = zip.goToFile("pack.png"); + if (!f) { + return png_invalid(); + } + auto data = f->readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + default: + qWarning() << "Invalid type for data pack parse task!"; + return false; + } +} + +bool validate(QFileInfo file) +{ + DataPack dp{ file }; + return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); +} + +bool validateResourcePack(QFileInfo file) +{ + ResourcePack rp{ file }; + return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); +} + +} // namespace DataPackUtils + +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {} + void LocalDataPackParseTask::executeTask() { - if (!DataPackUtils::process(m_data_pack)) + if (!DataPackUtils::process(m_data_pack)) { + emitFailed("process failed"); return; + } - if (m_aborted) - emitAborted(); - else - emitSucceeded(); + emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 12fd8c82c..82bb507e7 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -32,34 +32,39 @@ namespace DataPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; -bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); -bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); -bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); -bool processMCMeta(DataPack& pack, QByteArray&& raw_data); +bool processMCMeta(DataPack* pack, QByteArray&& raw_data); + +QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); + +bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const DataPack* pack); /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); +/** Checks whether a file is valid as a resource pack or not. */ +bool validateResourcePack(QFileInfo file); + } // namespace DataPackUtils class LocalDataPackParseTask : public Task { Q_OBJECT public: - LocalDataPackParseTask(int token, DataPack& dp); - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override; + LocalDataPackParseTask(int token, DataPack* dp); void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; - DataPack& m_data_pack; - - bool m_aborted = false; + DataPack* m_data_pack; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index d15bb094f..59d3876b3 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -1,8 +1,6 @@ #include "LocalModParseTask.h" #include -#include -#include #include #include #include @@ -13,10 +11,11 @@ #include "FileSystem.h" #include "Json.h" +#include "archive/ArchiveReader.h" #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" -static QRegularExpression newlineRegex("\r\n|\n|\r"); +static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); namespace ModUtils { @@ -27,7 +26,7 @@ namespace ModUtils { // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc ModDetails ReadMCModInfo(QByteArray contents) { - auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails { + auto getInfoFromArray = [](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { return {}; } @@ -75,11 +74,11 @@ ModDetails ReadMCModInfo(QByteArray contents) val = jsonDoc.object().value("modListVersion"); } - int version = Json::ensureInteger(val, -1); + int version = val.toInt(-1); // Some mods set the number with "", so it's a String instead if (version < 0) - version = Json::ensureString(val, "").toInt(); + version = val.toString("").toInt(); if (version != 2) { qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); @@ -298,7 +297,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents) QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); - auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); + auto schemaVersion = object.value("schema_version").toInt(); // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md if (schemaVersion == 1) { @@ -307,17 +306,17 @@ ModDetails ReadQuiltModInfo(QByteArray contents) details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); details.version = Json::requireString(modInfo.value("version"), "Mod version"); - auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + auto modMetadata = modInfo.value("metadata").toObject(); - details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); - details.description = Json::ensureString(modMetadata.value("description")); + details.name = modMetadata.value("name").toString(details.mod_id); + details.description = modMetadata.value("description").toString(); - auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + auto modContributors = modMetadata.value("contributors").toObject(); // We don't really care about the role of a contributor here details.authors += modContributors.keys(); - auto modContact = Json::ensureObject(modMetadata.value("contact")); + auto modContact = modMetadata.value("contact").toObject(); if (modContact.contains("homepage")) { details.homeurl = Json::requireString(modContact.value("homepage")); @@ -470,32 +469,33 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); - QuaZipFile file(&zip); + bool baseForgePopulated = false; + bool isNilMod = false; + bool isValid = false; + QString manifestVersion = {}; + QByteArray nilData = {}; + QString nilFilePath = {}; - if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } + if (!zip.parse([&details, &baseForgePopulated, &manifestVersion, &isValid, &nilData, &isNilMod, &nilFilePath]( + MMCZip::ArchiveReader::File* file, bool& stop) { + auto filePath = file->filename(); - details = ReadMCModTOML(file.readAll()); - file.close(); - - // to replace ${file.jarVersion} with the actual version, as needed - if (details.version == "${file.jarVersion}") { - if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; + if (filePath == "META-INF/mods.toml" || filePath == "META-INF/neoforge.mods.toml") { + details = ReadMCModTOML(file->readAll()); + isValid = true; + if (details.version == "${file.jarVersion}" && !manifestVersion.isEmpty()) { + details.version = manifestVersion; } - + stop = details.version != "${file.jarVersion}"; + baseForgePopulated = true; + return true; + } + if (filePath == "META-INF/MANIFEST.MF") { // quick and dirty line-by-line parser - auto manifestLines = QString(file.readAll()).split(newlineRegex); - QString manifestVersion = ""; + auto manifestLines = QString(file->readAll()).split(s_newlineRegex); + manifestVersion = ""; for (auto& line : manifestLines) { if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); @@ -508,94 +508,64 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") { manifestVersion = "NONE"; } - - details.version = manifestVersion; - - file.close(); + if (baseForgePopulated) { + details.version = manifestVersion; + stop = true; + } + return true; } - } - - zip.close(); - mod.setDetails(details); - - return true; - } else if (zip.setCurrentFile("mcmod.info")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("quilt.mod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadQuiltModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("fabric.mod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadFabricModInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("forgeversion.properties")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } - - details = ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); - - mod.setDetails(details); - return true; - } else if (zip.setCurrentFile("META-INF/nil/mappings.json")) { - // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename - // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time - - QString foundNilMeta; - for (auto& fname : zip.getFileNameList()) { - // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file - if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") { - foundNilMeta = fname; - break; + if (filePath == "mcmod.info") { + details = ReadMCModInfo(file->readAll()); + isValid = true; + stop = true; + return true; } - } - - if (zip.setCurrentFile(foundNilMeta)) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; + if (filePath == "quilt.mod.json") { + details = ReadQuiltModInfo(file->readAll()); + isValid = true; + stop = true; + return true; } - - details = ReadNilModInfo(file.readAll(), foundNilMeta); - file.close(); - zip.close(); - - mod.setDetails(details); + if (filePath == "fabric.mod.json") { + details = ReadFabricModInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "forgeversion.properties") { + details = ReadForgeInfo(file->readAll()); + isValid = true; + stop = true; + return true; + } + if (filePath == "META-INF/nil/mappings.json") { + // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename + // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time + isNilMod = true; + stop = !nilFilePath.isEmpty(); + file->skip(); + return true; + } + // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file + if (filePath.endsWith(".nilmod.css") && filePath != "nilloader.nilmod.css") { + nilData = file->readAll(); + nilFilePath = filePath; + stop = isNilMod; + return true; + } + file->skip(); return true; - } + })) { + return false; + } + if (isNilMod) { + details = ReadNilModInfo(nilData, nilFilePath); + isValid = true; + } + if (isValid) { + mod.setDetails(details); + return true; } - - zip.close(); return false; // no valid mod found in archive } @@ -624,25 +594,14 @@ bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) { - if (!file.open(QIODevice::ReadOnly)) { - zip.close(); - return false; - } + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); - details = ReadLiteModInfo(file.readAll()); - file.close(); + if (auto file = zip.goToFile("litemod.json"); file) { + details = ReadLiteModInfo(file->readAll()); mod.setDetails(details); return true; } - zip.close(); return false; // no valid litemod.json found in archive } @@ -700,24 +659,13 @@ bool loadIconFile(const Mod& mod, QPixmap* pixmap) return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { - QuaZip zip(mod.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); - - QuaZipFile file(&zip); - - if (zip.setCurrentFile(mod.iconPath())) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); - } - - auto data = file.readAll(); + MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); + auto file = zip.goToFile(mod.iconPath()); + if (file) { + auto data = file->readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); - file.close(); if (!icon_result) { return png_invalid("invalid png image"); // icon png invalid } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 91ee6f253..cbe009376 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -39,18 +39,13 @@ class LocalModParseTask : public Task { using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; - [[nodiscard]] int token() const { return m_token; } - - private: - void processAsZip(); - void processAsFolder(); - void processAsLitemod(); + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp deleted file mode 100644 index b011bf69e..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ /dev/null @@ -1,380 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * 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 "LocalResourcePackParseTask.h" - -#include "FileSystem.h" -#include "Json.h" - -#include -#include -#include - -#include - -namespace ResourcePackUtils { - -bool process(ResourcePack& pack, ProcessingLevel level) -{ - switch (pack.type()) { - case ResourceType::FOLDER: - return ResourcePackUtils::processFolder(pack, level); - case ResourceType::ZIPFILE: - return ResourcePackUtils::processZIP(pack, level); - default: - qWarning() << "Invalid type for resource pack parse task!"; - return false; - } -} - -bool processFolder(ResourcePack& pack, ProcessingLevel level) -{ - Q_ASSERT(pack.type() == ResourceType::FOLDER); - - auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; - return false; // the mcmeta is not optional - }; - - QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); - if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { - QFile mcmeta_file(mcmeta_file_info.filePath()); - if (!mcmeta_file.open(QIODevice::ReadOnly)) - return mcmeta_invalid(); // can't open mcmeta file - - auto data = mcmeta_file.readAll(); - - bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); - - mcmeta_file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid - } - } else { - return mcmeta_invalid(); // mcmeta file isn't a valid file - } - - QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets")); - if (!assets_dir_info.exists() || !assets_dir_info.isDir()) { - return false; // assets dir does not exists or isn't valid - } - - if (level == ProcessingLevel::BasicInfoOnly) { - return true; // only need basic info already checked - } - - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; - - QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file - - auto data = pack_png_file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. - } - - return true; // all tests passed -} - -bool processZIP(ResourcePack& pack, ProcessingLevel level) -{ - Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - - auto mcmeta_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; - return false; // the mcmeta is not optional - }; - - if (zip.setCurrentFile("pack.mcmeta")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return mcmeta_invalid(); - } - - auto data = file.readAll(); - - bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); - - file.close(); - if (!mcmeta_result) { - return mcmeta_invalid(); // mcmeta invalid - } - } else { - return mcmeta_invalid(); // could not set pack.mcmeta as current file. - } - - QuaZipDir zipDir(&zip); - if (!zipDir.exists("/assets")) { - return false; // assets dir does not exists at zip root - } - - if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); - return true; // only need basic info already checked - } - - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; - - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - zip.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. - } - - zip.close(); - return true; -} - -QString buildStyle(const QJsonObject& obj) -{ - QStringList styles; - if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { - styles << QString("color: %1;").arg(color); - } - if (obj.contains("bold")) { - QString weight = "normal"; - if (Json::ensureBoolean(obj, QStringLiteral("bold"), false)) { - weight = "bold"; - } - styles << QString("font-weight: %1;").arg(weight); - } - if (obj.contains("italic")) { - QString style = "normal"; - if (Json::ensureBoolean(obj, QStringLiteral("italic"), false)) { - style = "italic"; - } - styles << QString("font-style: %1;").arg(style); - } - - return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); -} - -QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) -{ - QString result; - for (auto current : value) - result += processComponent(current, strikethrough, underline); - return result; -} - -QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) -{ - underline = Json::ensureBoolean(obj, QStringLiteral("underlined"), underline); - strikethrough = Json::ensureBoolean(obj, QStringLiteral("strikethrough"), strikethrough); - - QString result = Json::ensureString(obj, "text"); - if (underline) { - result = QString("%1").arg(result); - } - if (strikethrough) { - result = QString("%1").arg(result); - } - // the extra needs to be a array - result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); - if (auto style = buildStyle(obj); !style.isEmpty()) { - result = QString("%2").arg(style, result); - } - if (obj.contains("clickEvent")) { - auto click_event = Json::ensureObject(obj, "clickEvent"); - auto action = Json::ensureString(click_event, "action"); - auto value = Json::ensureString(click_event, "value"); - if (action == "open_url" && !value.isEmpty()) { - result = QString("%2").arg(value, result); - } - } - return result; -} - -QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) -{ - if (value.isString()) { - return value.toString(); - } - if (value.isBool()) { - return value.toBool() ? "true" : "false"; - } - if (value.isDouble()) { - return QString::number(value.toDouble()); - } - if (value.isArray()) { - return processComponent(value.toArray(), strikethrough, underline); - } - if (value.isObject()) { - return processComponent(value.toObject(), strikethrough, underline); - } - qWarning() << "Invalid component type!"; - return {}; -} - -// https://minecraft.wiki/w/Raw_JSON_text_format -// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta -bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) -{ - try { - auto json_doc = QJsonDocument::fromJson(raw_data); - auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); - - pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - - pack.setDescription(processComponent(pack_obj.value("description"))); - - } catch (Json::JsonException& e) { - qWarning() << "JsonException: " << e.what() << e.cause(); - return false; - } - return true; -} - -bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data) -{ - auto img = QImage::fromData(raw_data); - if (!img.isNull()) { - pack.setImage(img); - } else { - qWarning() << "Failed to parse pack.png."; - return false; - } - return true; -} - -bool processPackPNG(const ResourcePack& pack) -{ - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; - return false; - }; - - switch (pack.type()) { - case ResourceType::FOLDER: { - QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file - - auto data = pack_png_file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - case ResourceType::ZIPFILE: { - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // could not set pack.mcmeta as current file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - default: - qWarning() << "Invalid type for resource pack parse task!"; - return false; - } -} - -bool validate(QFileInfo file) -{ - ResourcePack rp{ file }; - return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); -} - -} // namespace ResourcePackUtils - -LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) : Task(false), m_token(token), m_resource_pack(rp) {} - -bool LocalResourcePackParseTask::abort() -{ - m_aborted = true; - return true; -} - -void LocalResourcePackParseTask::executeTask() -{ - if (!ResourcePackUtils::process(m_resource_pack)) { - emitFailed("this is not a resource pack"); - return; - } - - if (m_aborted) - emitAborted(); - else - emitSucceeded(); -} diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h deleted file mode 100644 index 97bf7b2ba..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -#include "minecraft/mod/ResourcePack.h" - -#include "tasks/Task.h" - -namespace ResourcePackUtils { - -enum class ProcessingLevel { Full, BasicInfoOnly }; - -bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); - -bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); - -QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); -bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); -bool processPackPNG(const ResourcePack& pack, QByteArray&& raw_data); - -/// processes ONLY the pack.png (rest of the pack may be invalid) -bool processPackPNG(const ResourcePack& pack); - -/** Checks whether a file is valid as a resource pack or not. */ -bool validate(QFileInfo file); -} // namespace ResourcePackUtils - -class LocalResourcePackParseTask : public Task { - Q_OBJECT - public: - LocalResourcePackParseTask(int token, ResourcePack& rp); - - [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override; - - void executeTask() override; - - [[nodiscard]] int token() const { return m_token; } - - private: - int m_token; - - ResourcePack& m_resource_pack; - - bool m_aborted = false; -}; diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index d5a090832..39e8a321b 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -25,54 +25,41 @@ #include "LocalDataPackParseTask.h" #include "LocalModParseTask.h" -#include "LocalResourcePackParseTask.h" #include "LocalShaderPackParseTask.h" #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" - -static const QMap s_packed_type_names = { { PackedResourceType::ResourcePack, QObject::tr("resource pack") }, - { PackedResourceType::TexturePack, QObject::tr("texture pack") }, - { PackedResourceType::DataPack, QObject::tr("data pack") }, - { PackedResourceType::ShaderPack, QObject::tr("shader pack") }, - { PackedResourceType::WorldSave, QObject::tr("world save") }, - { PackedResourceType::Mod, QObject::tr("mod") }, - { PackedResourceType::UNKNOWN, QObject::tr("unknown") } }; +#include "modplatform/ResourceType.h" namespace ResourceUtils { -PackedResourceType identify(QFileInfo file) +ModPlatform::ResourceType identify(QFileInfo file) { if (file.exists() && file.isFile()) { if (ModUtils::validate(file)) { // mods can contain resource and data packs so they must be tested first qDebug() << file.fileName() << "is a mod"; - return PackedResourceType::Mod; - } else if (ResourcePackUtils::validate(file)) { + return ModPlatform::ResourceType::Mod; + } else if (DataPackUtils::validateResourcePack(file)) { qDebug() << file.fileName() << "is a resource pack"; - return PackedResourceType::ResourcePack; + return ModPlatform::ResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { qDebug() << file.fileName() << "is a pre 1.6 texture pack"; - return PackedResourceType::TexturePack; + return ModPlatform::ResourceType::TexturePack; } else if (DataPackUtils::validate(file)) { qDebug() << file.fileName() << "is a data pack"; - return PackedResourceType::DataPack; + return ModPlatform::ResourceType::DataPack; } else if (WorldSaveUtils::validate(file)) { qDebug() << file.fileName() << "is a world save"; - return PackedResourceType::WorldSave; + return ModPlatform::ResourceType::World; } else if (ShaderPackUtils::validate(file)) { qDebug() << file.fileName() << "is a shader pack"; - return PackedResourceType::ShaderPack; + return ModPlatform::ResourceType::ShaderPack; } else { qDebug() << "Can't Identify" << file.fileName(); } } else { qDebug() << "Can't find" << file.absolutePath(); } - return PackedResourceType::UNKNOWN; -} - -QString getPackedTypeName(PackedResourceType type) -{ - return s_packed_type_names.constFind(type).value(); + return ModPlatform::ResourceType::Unknown; } } // namespace ResourceUtils diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h index 7385d24b0..dc3aeb025 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.h +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -21,17 +21,9 @@ #pragma once -#include - -#include #include -#include +#include "modplatform/ResourceType.h" -enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; namespace ResourceUtils { -static const std::set ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack, - PackedResourceType::TexturePack, PackedResourceType::ShaderPack, - PackedResourceType::WorldSave, PackedResourceType::Mod }; -PackedResourceType identify(QFileInfo file); -QString getPackedTypeName(PackedResourceType type); +ModPlatform::ResourceType identify(QFileInfo file); } // namespace ResourceUtils diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp similarity index 52% rename from launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp rename to launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp index 4352fad91..02c73f858 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -#include "LocalModUpdateTask.h" +#include "LocalResourceUpdateTask.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" @@ -26,41 +26,47 @@ #include #endif -LocalModUpdateTask::LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version) - : m_index_dir(index_dir), m_mod(mod), m_mod_version(mod_version) +LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version) + : m_index_dir(index_dir), m_project(project), m_version(version) { // Ensure a '.index' folder exists in the mods folder, and create it if it does not if (!FS::ensureFolderPathExists(index_dir.path())) { - emitFailed(QString("Unable to create index for mod %1!").arg(m_mod.name)); + emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); } #ifdef Q_OS_WIN32 - SetFileAttributesW(index_dir.path().toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + std::wstring wpath = index_dir.path().toStdWString(); + if (index_dir.dirName().startsWith('.')) { + SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + } else { + // fix shaderpacks folder being hidden by Prism Launcher 10.0.1 + SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_NORMAL); + } #endif } -void LocalModUpdateTask::executeTask() +void LocalResourceUpdateTask::executeTask() { - setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); - auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId); + auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); if (old_metadata.isValid()) { - emit hasOldMod(old_metadata.name, old_metadata.filename); - if (m_mod.slug.isEmpty()) - m_mod.slug = old_metadata.slug; + emit hasOldResource(old_metadata.name, old_metadata.filename); + if (m_project.slug.isEmpty()) + m_project.slug = old_metadata.slug; } - auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); + auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); if (pw_mod.isValid()) { Metadata::update(m_index_dir, pw_mod); emitSucceeded(); } else { - qCritical() << "Tried to update an invalid mod!"; + qCritical() << "Tried to update an invalid resource!"; emitFailed(tr("Invalid metadata")); } } -auto LocalModUpdateTask::abort() -> bool +auto LocalResourceUpdateTask::abort() -> bool { emitAborted(); return true; diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h similarity index 74% rename from launcher/minecraft/mod/tasks/LocalModUpdateTask.h rename to launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h index 5447083ba..f8869258e 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h @@ -23,12 +23,12 @@ #include "modplatform/ModIndex.h" #include "tasks/Task.h" -class LocalModUpdateTask : public Task { +class LocalResourceUpdateTask : public Task { Q_OBJECT public: - using Ptr = shared_qobject_ptr; + using Ptr = shared_qobject_ptr; - explicit LocalModUpdateTask(QDir index_dir, ModPlatform::IndexedPack& mod, ModPlatform::IndexedVersion& mod_version); + explicit LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version); auto canAbort() const -> bool override { return true; } auto abort() -> bool override; @@ -38,10 +38,10 @@ class LocalModUpdateTask : public Task { void executeTask() override; signals: - void hasOldMod(QString name, QString filename); + void hasOldResource(QString name, QString filename); private: QDir m_index_dir; - ModPlatform::IndexedPack m_mod; - ModPlatform::IndexedVersion m_mod_version; + ModPlatform::IndexedPack m_project; + ModPlatform::IndexedVersion m_version; }; diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp index a6ecc5353..3a6b11b69 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -22,10 +22,7 @@ #include "LocalShaderPackParseTask.h" #include "FileSystem.h" - -#include -#include -#include +#include "archive/ArchiveReader.h" namespace ShaderPackUtils { @@ -63,25 +60,41 @@ bool processZIP(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); + if (!zip.collectFiles(false)) return false; // can't open zip file - QuaZipFile file(&zip); - - QuaZipDir zipDir(&zip); - if (!zipDir.exists("/shaders")) { - return false; // assets dir does not exists at zip root + if (!zip.exists("/shaders")) { + // assets dir does not exists at zip root, but shader packs + // will sometimes be a zip file containing a folder with the + // actual contents in it. This happens + // e.g. when the shader pack is downloaded as code + // from Github. so other than "/shaders", we + // could also check for a "shaders" folder one level deep. + + QStringList files = zip.getFiles(); + + // the assumption here is that there is just one + // folder with the "shader" subfolder. In case + // there are multiple, the first one is picked. + bool isShaderPresent = false; + for (QString f : files) { + if (f.contains("/shaders/", Qt::CaseInsensitive)) { + isShaderPresent = true; + break; + } + } + + if (!isShaderPresent) + // assets dir does not exist. + return false; } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } - zip.close(); - return true; } diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h index 6be2183cd..55d77f33b 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -46,12 +46,12 @@ class LocalShaderPackParseTask : public Task { public: LocalShaderPackParseTask(int token, ShaderPack& sp); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index 18020808a..106d7c323 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -20,9 +20,7 @@ #include "LocalTexturePackParseTask.h" #include "FileSystem.h" - -#include -#include +#include "archive/ArchiveReader.h" #include @@ -91,55 +89,26 @@ bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; - - QuaZipFile file(&zip); + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); + bool packProcessed = false; + bool iconProcessed = false; - if (zip.setCurrentFile("pack.txt")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return false; + return zip.parse([&packProcessed, &iconProcessed, &pack, level](MMCZip::ArchiveReader::File* file, bool& stop) { + if (!packProcessed && file->filename() == "pack.txt") { + packProcessed = true; + auto data = file->readAll(); + stop = packProcessed && (iconProcessed || level == ProcessingLevel::BasicInfoOnly); + return TexturePackUtils::processPackTXT(pack, std::move(data)); } - - auto data = file.readAll(); - - bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); - - file.close(); - if (!packTXT_result) { - return false; + if (!iconProcessed && file->filename() == "pack.png") { + iconProcessed = true; + auto data = file->readAll(); + stop = packProcessed && iconProcessed; + return TexturePackUtils::processPackPNG(pack, std::move(data)); } - } - - if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); + file->skip(); return true; - } - - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return false; - } - - auto data = file.readAll(); - - bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - zip.close(); - if (!packPNG_result) { - return false; - } - } - - zip.close(); - - return true; + }); } bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) @@ -189,32 +158,19 @@ bool processPackPNG(const TexturePack& pack) return false; } case ResourceType::ZIPFILE: { - QuaZip zip(pack.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } + MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); - auto data = file.readAll(); + auto file = zip.goToFile("pack.png"); + if (file) { + auto data = file->readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); - file.close(); if (!pack_png_result) { - zip.close(); return png_invalid(); // pack.png invalid } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. } - return false; + return png_invalid(); // could not set pack.mcmeta as current file. } default: qWarning() << "Invalid type for resource pack parse task!"; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 1341590f2..b9cc1ea54 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -50,12 +50,12 @@ class LocalTexturePackParseTask : public Task { public: LocalTexturePackParseTask(int token, TexturePack& rp); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp index 74d8d8d60..50f7bbfa1 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -23,13 +23,11 @@ #include "LocalWorldSaveParseTask.h" #include "FileSystem.h" - -#include -#include -#include +#include "archive/ArchiveReader.h" #include #include +#include namespace WorldSaveUtils { @@ -105,22 +103,40 @@ bool processFolder(WorldSave& save, ProcessingLevel level) /// QString , /// bool /// ) -static std::tuple contains_level_dat(QuaZip& zip) +static std::tuple contains_level_dat(QString fileName) { + MMCZip::ArchiveReader zip(fileName); + if (!zip.collectFiles()) { + return std::make_tuple(false, "", false); + } bool saves = false; - QuaZipDir zipDir(&zip); - if (zipDir.exists("/saves")) { + if (zip.exists("/saves")) { saves = true; - zipDir.cd("/saves"); } - for (auto const& entry : zipDir.entryList()) { - zipDir.cd(entry); - if (zipDir.exists("level.dat")) { - return std::make_tuple(true, entry, saves); + for (auto file : zip.getFiles()) { + QString relativePath = file; + if (saves) { + if (!relativePath.startsWith("saves/", Qt::CaseInsensitive)) + continue; + relativePath = relativePath.mid(QString("saves/").length()); + } + if (!relativePath.endsWith("/level.dat", Qt::CaseInsensitive)) + continue; + + int slashIndex = relativePath.indexOf('/'); + if (slashIndex == -1) + continue; // malformed: no slash between saves/ and level.dat + + QString worldName = relativePath.left(slashIndex); + QString remaining = relativePath.mid(slashIndex + 1); + + // Check that there's nothing between worldName/ and level.dat + if (remaining == "level.dat") { + return std::make_tuple(true, worldName, saves); } - zipDir.cd(".."); } + return std::make_tuple(false, "", saves); } @@ -128,19 +144,14 @@ bool processZIP(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::ZIPFILE); - QuaZip zip(save.fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); - - if (save_dir_name.endsWith("/")) { - save_dir_name.chop(1); - } + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(save.fileinfo().filePath()); if (!found) { return false; } + if (save_dir_name.endsWith("/")) { + save_dir_name.chop(1); + } save.setSaveDirName(save_dir_name); @@ -151,14 +162,11 @@ bool processZIP(WorldSave& save, ProcessingLevel level) } if (level == ProcessingLevel::BasicInfoOnly) { - zip.close(); return true; // only need basic info already checked } // reserved for more intensive processing - zip.close(); - return true; } @@ -180,8 +188,10 @@ bool LocalWorldSaveParseTask::abort() void LocalWorldSaveParseTask::executeTask() { - if (!WorldSaveUtils::process(m_save)) + if (!WorldSaveUtils::process(m_save)) { + emitFailed("this is not a world"); return; + } if (m_aborted) emitAborted(); diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h index 12f677b02..42faf51c5 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -46,12 +46,12 @@ class LocalWorldSaveParseTask : public Task { public: LocalWorldSaveParseTask(int token, WorldSave& save); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp similarity index 57% rename from launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp rename to launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 2c3439ca4..3b98e053b 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -34,7 +34,7 @@ * limitations under the License. */ -#include "ModFolderLoadTask.h" +#include "ResourceFolderLoadTask.h" #include "Application.h" #include "FileSystem.h" @@ -42,17 +42,22 @@ #include -ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) +ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function) : Task(false) - , m_mods_dir(mods_dir) + , m_resource_dir(resource_dir) , m_index_dir(index_dir) , m_is_indexed(is_indexed) , m_clean_orphan(clean_orphan) + , m_create_func(create_function) , m_result(new Result()) , m_thread_to_spawn_into(thread()) {} -void ModFolderLoadTask::executeTask() +void ResourceFolderLoadTask::executeTask() { if (thread() != m_thread_to_spawn_into) connect(this, &Task::finished, this->thread(), &QThread::quit); @@ -63,8 +68,8 @@ void ModFolderLoadTask::executeTask() } // Read JAR files that don't have metadata - m_mods_dir.refresh(); - for (auto entry : m_mods_dir.entryInfoList()) { + m_resource_dir.refresh(); + for (auto entry : m_resource_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { continue; @@ -74,32 +79,33 @@ void ModFolderLoadTask::executeTask() FS::move(filePath, newFilePath); entry = QFileInfo(newFilePath); } - Mod* mod(new Mod(entry)); - if (mod->enabled()) { - if (m_result->mods.contains(mod->internal_id())) { - m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); + Resource* resource = m_create_func(entry); + + if (resource->enabled()) { + if (m_result->resources.contains(resource->internal_id())) { + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); // Delete the object we just created, since a valid one is already in the mods list. - delete mod; + delete resource; } else { - m_result->mods[mod->internal_id()].reset(std::move(mod)); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } else { - QString chopped_id = mod->internal_id().chopped(9); - if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod->internal_id()].reset(std::move(mod)); + QString chopped_id = resource->internal_id().chopped(9); + if (m_result->resources.contains(chopped_id)) { + m_result->resources[resource->internal_id()].reset(resource); - auto metadata = m_result->mods[chopped_id]->metadata(); + auto metadata = m_result->resources[chopped_id]->metadata(); if (metadata) { - mod->setMetadata(*metadata); + resource->setMetadata(*metadata); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); - m_result->mods.remove(chopped_id); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + m_result->resources.remove(chopped_id); } } else { - m_result->mods[mod->internal_id()].reset(std::move(mod)); - m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } } @@ -107,17 +113,17 @@ void ModFolderLoadTask::executeTask() // Remove orphan metadata to prevent issues // See https://github.com/PolyMC/PolyMC/issues/996 if (m_clean_orphan) { - QMutableMapIterator iter(m_result->mods); + QMutableMapIterator iter(m_result->resources); while (iter.hasNext()) { - auto mod = iter.next().value(); - if (mod->status() == ModStatus::NotInstalled) { - mod->destroy(m_index_dir, false, false); + auto resource = iter.next().value(); + if (resource->status() == ResourceStatus::NOT_INSTALLED) { + resource->destroy(m_index_dir, false, false); iter.remove(); } } } - for (auto mod : m_result->mods) + for (auto mod : m_result->resources) mod->moveToThread(m_thread_to_spawn_into); if (m_aborted) @@ -126,18 +132,22 @@ void ModFolderLoadTask::executeTask() emitSucceeded(); } -void ModFolderLoadTask::getFromMetadata() +void ResourceFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); for (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* mod = new Mod(m_mods_dir, metadata); - mod->setStatus(ModStatus::NotInstalled); - m_result->mods[mod->internal_id()].reset(std::move(mod)); + auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); + resource->setMetadata(metadata); + resource->setStatus(ResourceStatus::NOT_INSTALLED); + m_result->resources[resource->internal_id()].reset(resource); } } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h similarity index 81% rename from launcher/minecraft/mod/tasks/ModFolderLoadTask.h rename to launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 4200ef6d9..7c872c13d 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -44,19 +44,23 @@ #include "minecraft/mod/Mod.h" #include "tasks/Task.h" -class ModFolderLoadTask : public Task { +class ResourceFolderLoadTask : public Task { Q_OBJECT public: struct Result { - QMap mods; + QMap resources; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } public: - ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false); + ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function create_function); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override { m_aborted.store(true); @@ -69,9 +73,10 @@ class ModFolderLoadTask : public Task { void getFromMetadata(); private: - QDir m_mods_dir, m_index_dir; + QDir m_resource_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; + std::function m_create_func; ResultPtr m_result; std::atomic m_aborted = false; diff --git a/launcher/minecraft/skins/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 017cb8dc2..54c77bf25 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -31,7 +31,7 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher.reset(new QFileSystemWatcher(this)); - is_watching = false; + m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); directoryChanged(path); @@ -39,12 +39,12 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA void SkinList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -54,11 +54,11 @@ void SkinList::startWatching() void SkinList::stopWatching() { save(); - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -67,7 +67,7 @@ void SkinList::stopWatching() bool SkinList::update() { - QVector newSkins; + QList newSkins; m_dir.refresh(); auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); @@ -75,9 +75,9 @@ bool SkinList::update() try { auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); const auto root = doc.object(); - auto skins = Json::ensureArray(root, "skins"); + auto skins = root["skins"].toArray(); for (auto jSkin : skins) { - SkinModel s(m_dir, Json::ensureObject(jSkin)); + SkinModel s(m_dir, jSkin.toObject()); if (s.isValid()) { newSkins << s; } @@ -142,7 +142,7 @@ bool SkinList::update() std::sort(newSkins.begin(), newSkins.end(), [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); beginResetModel(); - m_skin_list.swap(newSkins); + m_skinList.swap(newSkins); endResetModel(); if (needsSave) save(); @@ -158,7 +158,7 @@ void SkinList::directoryChanged(const QString& path) if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); - if (is_watching) + if (m_isWatching) stopWatching(); startWatching(); } @@ -172,9 +172,9 @@ void SkinList::fileChanged(const QString& path) if (!checkfile.exists()) return; - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) { - m_skin_list[i].refresh(); + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) { + m_skinList[i].refresh(); dataChanged(index(i), index(i)); break; } @@ -235,12 +235,17 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int row = index.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return QVariant(); - auto skin = m_skin_list[row]; + auto skin = m_skinList[row]; switch (role) { - case Qt::DecorationRole: - return skin.getTexture(); + case Qt::DecorationRole: { + auto preview = skin.getPreview(); + if (preview.isNull()) { + preview = skin.getTexture(); + } + return preview; + } case Qt::DisplayRole: return skin.name(); case Qt::UserRole: @@ -254,7 +259,7 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int SkinList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : m_skin_list.size(); + return parent.isValid() ? 0 : m_skinList.size(); } void SkinList::installSkins(const QStringList& iconFiles) @@ -263,6 +268,26 @@ void SkinList::installSkins(const QStringList& iconFiles) installSkin(file); } +QString getUniqueFile(const QString& root, const QString& file) +{ + auto result = FS::PathCombine(root, file); + if (!QFileInfo::exists(result)) { + return result; + } + + QString baseName = QFileInfo(file).completeBaseName(); + QString extension = QFileInfo(file).suffix(); + int tries = 0; + while (QFileInfo::exists(result)) { + if (++tries > 256) + return {}; + + QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); + result = FS::PathCombine(root, key); + } + + return result; +} QString SkinList::installSkin(const QString& file, const QString& name) { if (file.isEmpty()) @@ -277,15 +302,15 @@ QString SkinList::installSkin(const QString& file, const QString& name) if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); - QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); return QFile::copy(file, target) ? "" : tr("Unable to copy file"); } int SkinList::getSkinIndex(const QString& key) const { - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].name() == key) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].name() == key) { return i; } } @@ -297,7 +322,7 @@ const SkinModel* SkinList::skin(const QString& key) const int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } SkinModel* SkinList::skin(const QString& key) @@ -305,22 +330,22 @@ SkinModel* SkinList::skin(const QString& key) int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } -bool SkinList::deleteSkin(const QString& key, const bool trash) +bool SkinList::deleteSkin(const QString& key, bool trash) { int idx = getSkinIndex(key); if (idx != -1) { - auto s = m_skin_list[idx]; + auto s = m_skinList[idx]; if (trash) { if (FS::trash(s.getPath(), nullptr)) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } } else if (QFile::remove(s.getPath())) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } @@ -332,7 +357,7 @@ void SkinList::save() { QJsonObject doc; QJsonArray arr; - for (auto s : m_skin_list) { + for (auto s : m_skinList) { arr << s.toJSON(); } doc["skins"] = arr; @@ -346,8 +371,8 @@ void SkinList::save() int SkinList::getSelectedAccountSkin() { const auto& skin = m_acct->accountData()->minecraftProfile.skin; - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].getURL() == skin.url) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getURL() == skin.url) { return i; } } @@ -361,12 +386,13 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) } int row = idx.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return false; - auto& skin = m_skin_list[row]; + auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { - skin.rename(newName); + if (!skin.rename(newName)) + return false; save(); } return true; @@ -375,18 +401,18 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) void SkinList::updateSkin(SkinModel* s) { auto done = false; - for (auto i = 0; i < m_skin_list.size(); i++) { - if (m_skin_list[i].getPath() == s->getPath()) { - m_skin_list[i].setCapeId(s->getCapeId()); - m_skin_list[i].setModel(s->getModel()); - m_skin_list[i].setURL(s->getURL()); + for (auto i = 0; i < m_skinList.size(); i++) { + if (m_skinList[i].getPath() == s->getPath()) { + m_skinList[i].setCapeId(s->getCapeId()); + m_skinList[i].setModel(s->getModel()); + m_skinList[i].setURL(s->getURL()); done = true; break; } } if (!done) { - beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); - m_skin_list.append(*s); + beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); + m_skinList.append(*s); endInsertRows(); } save(); diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index 66af6a17b..9db476f3c 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -43,7 +43,7 @@ class SkinList : public QAbstractListModel { virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; - bool deleteSkin(const QString& key, const bool trash); + bool deleteSkin(const QString& key, bool trash); void installSkins(const QStringList& iconFiles); QString installSkin(const QString& file, const QString& name = {}); @@ -73,8 +73,8 @@ class SkinList : public QAbstractListModel { private: shared_qobject_ptr m_watcher; - bool is_watching; - QVector m_skin_list; + bool m_isWatching; + QList m_skinList; QDir m_dir; MinecraftAccountPtr m_acct; -}; \ No newline at end of file +}; diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index 937864e2c..ca2545e05 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2025 Trial97 + * Copyright (c) 2025 Rinth, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,25 +19,165 @@ #include "SkinModel.h" #include -#include #include -#include #include "FileSystem.h" -#include "Json.h" -SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {} +static void setAlpha(QImage& image, const QRect& region, const int alpha) +{ + for (int y = region.top(); y < region.bottom(); ++y) { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = region.left(); x < region.right(); ++x) { + QRgb pixel = line[x]; + line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), alpha); + } + } +} + +static void doNotchTransparencyHack(QImage& image) +{ + for (int y = 0; y < 32; y++) { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = 32; x < 64; x++) { + if (qAlpha(line[x]) < 128) { + return; + } + } + } + + setAlpha(image, { 32, 0, 32, 32 }, 0); +} + +static QImage improveSkin(QImage skin) +{ + int height = skin.height(); + int width = skin.width(); + if (width != 64 || (height != 32 && height != 64)) { // this is no minecraft skin + return 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) { + skin = skin.convertToFormat(QImage::Format_ARGB32); + } + + auto isLegacy = height == 32; // old format + if (isLegacy) { + auto newSkin = QImage(QSize(64, 64), skin.format()); + newSkin.fill(Qt::transparent); + QPainter p(&newSkin); + p.drawImage(0, 0, skin); + + auto copyRect = [&p, &newSkin](int startX, int startY, int offsetX, int offsetY, int sizeX, int sizeY) { + QImage region = newSkin.copy(startX, startY, sizeX, sizeY); + region = region.mirrored(true, false); + + p.drawImage(startX + offsetX, startY + offsetY, region); + }; + static const struct { + int x; + int y; + int offsetX; + int offsetY; + int width; + int height; + } faces[] = { + { 4, 16, 16, 32, 4, 4 }, { 8, 16, 16, 32, 4, 4 }, { 0, 20, 24, 32, 4, 12 }, { 4, 20, 16, 32, 4, 12 }, + { 8, 20, 8, 32, 4, 12 }, { 12, 20, 16, 32, 4, 12 }, { 44, 16, -8, 32, 4, 4 }, { 48, 16, -8, 32, 4, 4 }, + { 40, 20, 0, 32, 4, 12 }, { 44, 20, -8, 32, 4, 12 }, { 48, 20, -16, 32, 4, 12 }, { 52, 20, -8, 32, 4, 12 }, + }; + + for (const auto& face : faces) { + copyRect(face.x, face.y, face.offsetX, face.offsetY, face.width, face.height); + } + doNotchTransparencyHack(newSkin); + skin = newSkin; + } + static const QRect opaqueParts[] = { + { 0, 0, 32, 16 }, + { 0, 16, 64, 16 }, + { 16, 48, 32, 16 }, + }; + + for (const auto& p : opaqueParts) { + setAlpha(skin, p, 255); + } + return skin; +} + +static QImage getSkin(const QString path) +{ + return improveSkin(QImage(path)); +} + +static QImage generatePreviews(QImage texture, bool slim) +{ + QImage preview(36, 36, QImage::Format_ARGB32); + preview.fill(Qt::transparent); + QPainter paint(&preview); + + // head + paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); + paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); + // torso + paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); + paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); + // right leg + paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); + paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); + // left leg + paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); + + auto armWidth = slim ? 3 : 4; + auto armPosX = slim ? 1 : 0; + // right arm + paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); + paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); + // left arm + paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); + paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); + + // back + // head + paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); + paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); + // torso + paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); + paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); + // right leg + paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); + paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); + // left leg + paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); + + // right arm + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); + // left arm + paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); + paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); + + return preview; +} +SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) +{ + m_preview = generatePreviews(m_texture, false); +} SkinModel::SkinModel(QDir skinDir, QJsonObject obj) - : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) + : m_capeId(obj["capeId"].toString()), m_model(Model::CLASSIC), m_url(obj["url"].toString()) { - auto name = Json::ensureString(obj, "name"); + auto name = obj["name"].toString(); - if (auto model = Json::ensureString(obj, "model"); model == "SLIM") { + if (auto model = obj["model"].toString(); model == "SLIM") { m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; - m_texture = QPixmap(m_path); + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } QString SkinModel::name() const @@ -47,7 +188,11 @@ QString SkinModel::name() const bool SkinModel::rename(QString newName) { auto info = QFileInfo(m_path); - m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + if (QFileInfo::exists(new_path)) { + return false; + } + m_path = new_path; return FS::move(info.absoluteFilePath(), m_path); } @@ -55,7 +200,7 @@ QJsonObject SkinModel::toJSON() const { QJsonObject obj; obj["name"] = name(); - obj["capeId"] = m_cape_id; + obj["capeId"] = m_capeId; obj["url"] = m_url; obj["model"] = getModelString(); return obj; @@ -76,3 +221,13 @@ bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } +void SkinModel::refresh() +{ + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} +void SkinModel::setModel(Model model) +{ + m_model = model; + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h index 46e9d6cf1..af8ca046f 100644 --- a/launcher/minecraft/skins/SkinModel.h +++ b/launcher/minecraft/skins/SkinModel.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include class SkinModel { public: @@ -35,23 +35,25 @@ class SkinModel { QString getModelString() const; bool isValid() const; QString getPath() const { return m_path; } - QPixmap getTexture() const { return m_texture; } - QString getCapeId() const { return m_cape_id; } + QImage getTexture() const { return m_texture; } + QImage getPreview() const { return m_preview; } + QString getCapeId() const { return m_capeId; } Model getModel() const { return m_model; } QString getURL() const { return m_url; } bool rename(QString newName); - void setCapeId(QString capeID) { m_cape_id = capeID; } - void setModel(Model model) { m_model = model; } + void setCapeId(QString capeID) { m_capeId = capeID; } + void setModel(Model model); void setURL(QString url) { m_url = url; } - void refresh() { m_texture = QPixmap(m_path); } + void refresh(); QJsonObject toJSON() const; private: QString m_path; - QPixmap m_texture; - QString m_cape_id; + QImage m_texture; + QImage m_preview; + QString m_capeId; Model m_model; QString m_url; -}; \ No newline at end of file +}; diff --git a/launcher/minecraft/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 acdddc833..74d545230 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -1,5 +1,6 @@ #include "AssetUpdateTask.h" +#include "BuildConfig.h" #include "launch/LaunchStep.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" @@ -71,7 +72,12 @@ void AssetUpdateTask::assetIndexFinished() auto job = index.getDownloadJob(); if (job) { - setStatus(tr("Getting the assets files from Mojang...")); + QString resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + QString source = tr("Mojang"); + if (resourceURL != BuildConfig.DEFAULT_RESOURCE_BASE) { + source = QUrl(resourceURL).host(); + } + setStatus(tr("Getting the asset files from %1...").arg(source)); downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 1581b32ee..85f4103cd 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -25,13 +25,13 @@ void LibrariesTask::executeTask() auto metacache = APPLICATION->metacache(); - auto processArtifactPool = [&](const QList& pool, QStringList& errors, const QString& localPath) { + auto processArtifactPool = [this, inst, metacache](const QList& pool, QStringList& errors, const QString& localPath) { for (auto lib : pool) { if (!lib) { emitFailed(tr("Null jar is specified in the metadata, aborting.")); 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); } diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index ff064c904..b9bed7ee1 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -1,6 +1,5 @@ #pragma once -#include "minecraft/mod/Mod.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "tasks/Task.h" @@ -12,13 +11,14 @@ class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(QList& mods, + CheckUpdateTask(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder) - : Task(), m_mods(mods), m_game_versions(mcVersions), m_loaders_list(loadersList), m_mods_folder(mods_folder) {}; + ResourceFolderModel* resourceModel) + : Task(), m_resources(resources), m_gameVersions(mcVersions), m_loadersList(std::move(loadersList)), m_resourceModel(resourceModel) + {} - struct UpdatableMod { + struct Update { QString name; QString old_hash; QString old_version; @@ -30,28 +30,28 @@ class CheckUpdateTask : public Task { bool enabled = true; public: - UpdatableMod(QString name, - QString old_h, - QString old_v, - QString new_v, - std::optional new_v_type, - QString changelog, - ModPlatform::ResourceProvider p, - shared_qobject_ptr t, - bool enabled = true) - : name(name) - , old_hash(old_h) - , old_version(old_v) - , new_version(new_v) - , new_version_type(new_v_type) - , changelog(changelog) + Update(QString name, + QString old_h, + QString old_v, + QString new_v, + std::optional new_v_type, + QString changelog, + ModPlatform::ResourceProvider p, + shared_qobject_ptr t, + bool enabled = true) + : name(std::move(name)) + , old_hash(std::move(old_h)) + , old_version(std::move(old_v)) + , new_version(std::move(new_v)) + , new_version_type(std::move(new_v_type)) + , changelog(std::move(changelog)) , provider(p) - , download(t) + , download(std::move(t)) , enabled(enabled) {} }; - auto getUpdatable() -> std::vector&& { return std::move(m_updatable); } + auto getUpdates() -> std::vector&& { return std::move(m_updates); } auto getDependencies() -> QList>&& { return std::move(m_deps); } public slots: @@ -61,14 +61,14 @@ class CheckUpdateTask : public Task { void executeTask() override = 0; signals: - void checkFailed(Mod* failed, QString reason, QUrl recover_url = {}); + void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); protected: - QList& m_mods; - std::list& m_game_versions; - QList m_loaders_list; - std::shared_ptr m_mods_folder; + QList& m_resources; + std::list& m_gameVersions; + QList m_loadersList; + ResourceFolderModel* m_resourceModel; - std::vector m_updatable; + std::vector m_updates; QList> m_deps; }; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index fb4a32918..8e2ad744b 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -6,8 +6,9 @@ #include "Application.h" #include "Json.h" +#include "QObjectPtr.h" #include "minecraft/mod/Mod.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -18,56 +19,57 @@ static ModrinthAPI modrinth_api; static FlameAPI flame_api; -EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::ResourceProvider prov) - : Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr) +EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_indexDir(dir), m_provider(prov), m_hashingTask(nullptr), m_currentTask(nullptr) { - auto hashTask = createNewHash(mod); + auto hashTask = createNewHash(resource); if (!hashTask) return; - connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); - connect(hashTask.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); m_hashingTask = hashTask; } -EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::ResourceProvider prov) - : Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) { auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); m_hashingTask = hashTask; - for (auto* mod : mods) { - auto hash_task = createNewHash(mod); + for (auto* resource : resources) { + auto hash_task = createNewHash(resource); if (!hash_task) continue; - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mods.insert(hash, mod); }); - connect(hash_task.get(), &Task::failed, [this, mod] { emitFail(mod, "", RemoveFromList::No); }); + connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); + connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); hashTask->addTask(hash_task); } } -EnsureMetadataTask::EnsureMetadataTask(QHash& mods, QDir dir, ModPlatform::ResourceProvider prov) - : Task(), m_mods(mods), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) + +EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) + : Task(), m_resources(resources), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) {} -Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod) +Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) { - if (!mod || !mod->valid() || mod->type() == ResourceType::FOLDER) + if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) return nullptr; - return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider); + return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider); } -QString EnsureMetadataTask::getExistingHash(Mod* mod) +QString EnsureMetadataTask::getExistingHash(Resource* resource) { // Check for already computed hashes // (linear on the number of mods vs. linear on the size of the mod's JAR) - auto it = m_mods.keyValueBegin(); - while (it != m_mods.keyValueEnd()) { - if ((*it).second == mod) + auto it = m_resources.keyValueBegin(); + while (it != m_resources.keyValueEnd()) { + if ((*it).second == resource) break; it++; } // We already have the hash computed - if (it != m_mods.keyValueEnd()) { + if (it != m_resources.keyValueEnd()) { return (*it).first; } @@ -80,32 +82,32 @@ bool EnsureMetadataTask::abort() // Prevent sending signals to a dead object disconnect(this, 0, 0, 0); - if (m_current_task) - return m_current_task->abort(); + if (m_currentTask) + return m_currentTask->abort(); return true; } void EnsureMetadataTask::executeTask() { - setStatus(tr("Checking if mods have metadata...")); + setStatus(tr("Checking if resources have metadata...")); - for (auto* mod : m_mods) { - if (!mod->valid()) { - qDebug() << "Mod" << mod->name() << "is invalid!"; - emitFail(mod); + for (auto* resource : m_resources) { + if (!resource->valid()) { + qDebug() << "Resource" << resource->name() << "is invalid!"; + emitFail(resource); continue; } // They already have the right metadata :o - if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) { - qDebug() << "Mod" << mod->name() << "already has metadata!"; - emitReady(mod); + if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { + qDebug() << "Resource" << resource->name() << "already has metadata!"; + emitReady(resource); continue; } // Folders don't have metadata - if (mod->type() == ResourceType::FOLDER) { - emitReady(mod); + if (resource->type() == ResourceType::FOLDER) { + emitReady(resource); } } @@ -121,9 +123,9 @@ void EnsureMetadataTask::executeTask() } auto invalidade_leftover = [this] { - for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++) - emitFail(mod.value(), mod.key(), RemoveFromList::No); - m_mods.clear(); + for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) + emitFail(resource.value(), resource.key(), RemoveFromList::No); + m_resources.clear(); emitSucceeded(); }; @@ -145,71 +147,65 @@ void EnsureMetadataTask::executeTask() return; } - connect(project_task.get(), &Task::finished, this, [=] { + connect(project_task.get(), &Task::finished, this, [this, invalidade_leftover, project_task] { invalidade_leftover(); project_task->deleteLater(); - if (m_current_task) - m_current_task.reset(); + if (m_currentTask) + m_currentTask.reset(); }); connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); - m_current_task = project_task; + m_currentTask = project_task; project_task->start(); }); - connect(version_task.get(), &Task::finished, [=] { - version_task->deleteLater(); - if (m_current_task) - m_current_task.reset(); - }); - - if (m_mods.size() > 1) + if (m_resources.size() > 1) setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); - else if (!m_mods.empty()) + else if (!m_resources.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") - .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_mods.begin().value()->name())); + .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); - m_current_task = version_task; + m_currentTask = version_task; version_task->start(); } -void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove) +void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove) { - if (!m) { - qCritical() << "Tried to mark a null mod as ready."; + if (!resource) { + qCritical() << "Tried to mark a null resource as ready."; if (!key.isEmpty()) - m_mods.remove(key); + m_resources.remove(key); return; } - qDebug() << QString("Generated metadata for %1").arg(m->name()); - emit metadataReady(m); + qDebug() << QString("Generated metadata for %1").arg(resource->name()); + emit metadataReady(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) - key = getExistingHash(m); - m_mods.remove(key); + key = getExistingHash(resource); + m_resources.remove(key); } } -void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove) +void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove) { - if (!m) { - qCritical() << "Tried to mark a null mod as failed."; + if (!resource) { + qCritical() << "Tried to mark a null resource as failed."; if (!key.isEmpty()) - m_mods.remove(key); + m_resources.remove(key); return; } - qDebug() << QString("Failed to generate metadata for %1").arg(m->name()); - emit metadataFailed(m); + qDebug() << QString("Failed to generate metadata for %1").arg(resource->name()); + emit metadataFailed(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) - key = getExistingHash(m); - m_mods.remove(key); + key = getExistingHash(resource); + m_resources.remove(key); } } @@ -220,7 +216,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); auto response = std::make_shared(); - auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); + auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response.get()); // Prevents unfortunate timings when aborting the task if (!ver_task) @@ -240,20 +236,20 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() try { auto entries = Json::requireObject(doc); - for (auto& hash : m_mods.keys()) { - auto mod = m_mods.find(hash).value(); + for (auto& hash : m_resources.keys()) { + auto resource = m_resources.find(hash).value(); try { auto entry = Json::requireObject(entries, hash); - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); - qDebug() << "Getting version for" << mod->name() << "from Modrinth"; + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); + qDebug() << "Getting version for" << resource->name() << "from Modrinth"; - m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); + m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } } } catch (Json::JsonException& e) { @@ -268,7 +264,7 @@ Task::Ptr EnsureMetadataTask::modrinthVersionsTask() Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; - for (auto const& data : m_temp_versions) + for (auto const& data : m_tempVersions) addonIds.insert(data.addonId.toString(), data.hash); auto response = std::make_shared(); @@ -277,9 +273,9 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { - proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); + proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response.get()); } else { - proj_task = modrinth_api.getProjects(addonIds.keys(), response); + proj_task = modrinth_api.getProjects(addonIds.keys(), response.get()); } // Prevents unfortunate timings when aborting the task @@ -325,24 +321,17 @@ Task::Ptr EnsureMetadataTask::modrinthProjectsTask() auto hash = addonIds.find(pack.addonId.toString()).value(); - auto mod_iter = m_mods.find(hash); - if (mod_iter == m_mods.end()) { + auto resource_iter = m_resources.find(hash); + if (resource_iter == m_resources.end()) { qWarning() << "Invalid project id from the API response."; continue; } - auto* mod = mod_iter.value(); - - try { - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + auto* resource = resource_iter.value(); - modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - qDebug() << entries; + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); - emitFail(mod); - } + updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } }); @@ -355,11 +344,11 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() auto response = std::make_shared(); QList fingerprints; - for (auto& murmur : m_mods.keys()) { + for (auto& murmur : m_resources.keys()) { fingerprints.push_back(murmur.toUInt()); } - auto ver_task = flame_api.matchFingerprints(fingerprints, response); + auto ver_task = flame_api.matchFingerprints(fingerprints, response.get()); connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; @@ -385,8 +374,8 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() } for (auto match : data_arr) { - auto match_obj = Json::ensureObject(match, {}); - auto file_obj = Json::ensureObject(match_obj, "file", {}); + auto match_obj = match.toObject(); + auto file_obj = match_obj["file"].toObject(); if (match_obj.isEmpty() || file_obj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; @@ -394,16 +383,16 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() return; } - auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); - auto mod = m_mods.find(fingerprint); - if (mod == m_mods.end()) { + auto fingerprint = QString::number(file_obj["fileFingerprint"].toInteger()); + auto resource = m_resources.find(fingerprint); + if (resource == m_resources.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name())); - m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); + m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } } catch (Json::JsonException& e) { @@ -418,9 +407,9 @@ Task::Ptr EnsureMetadataTask::flameVersionsTask() Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; - for (auto const& hash : m_mods.keys()) { - if (m_temp_versions.contains(hash)) { - auto data = m_temp_versions.find(hash).value(); + for (auto const& hash : m_resources.keys()) { + if (m_tempVersions.contains(hash)) { + auto data = m_tempVersions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) @@ -434,9 +423,9 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { - proj_task = flame_api.getProject(*addonIds.keyBegin(), response); + proj_task = flame_api.getProject(*addonIds.keyBegin(), response.get()); } else { - proj_task = flame_api.getProjects(addonIds.keys(), response); + proj_task = flame_api.getProjects(addonIds.keys(), response.get()); } // Prevents unfortunate timings when aborting the task @@ -465,21 +454,21 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto hash = addonIds.find(id).value(); - auto mod = m_mods.find(hash).value(); + auto resource = m_resources.find(hash).value(); + ModPlatform::IndexedPack pack; try { - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); - ModPlatform::IndexedPack pack; FlameMod::loadIndexedPack(pack, entry_obj); - flameCallback(pack, m_temp_versions.find(hash).value(), mod); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; - emitFail(mod); + emitFail(resource); } + updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } } catch (Json::JsonException& e) { qDebug() << e.cause(); @@ -490,74 +479,38 @@ Task::Ptr EnsureMetadataTask::flameProjectsTask() return proj_task; } -void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) +void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource) { - // Prevent file name mismatch - ver.fileName = mod->fileinfo().fileName(); - if (ver.fileName.endsWith(".disabled")) - ver.fileName.chop(9); - - QDir tmp_index_dir(m_index_dir); + try { + // Prevent file name mismatch + ver.fileName = resource->fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; + auto task = makeShared(m_indexDir, pack, ver); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); }); - update_metadata.start(); + m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task; + task->start(); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); - if (!update_metadata.isFinished()) - loop.exec(); + emitFail(resource); } +} - auto metadata = Metadata::get(tmp_index_dir, pack.slug); +void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource) +{ + QDir tmpIndexDir(m_indexDir); + auto metadata = Metadata::get(tmpIndexDir, pack.slug); if (!metadata.isValid()) { qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); + emitFail(resource); return; } - mod->setMetadata(metadata); + resource->setMetadata(metadata); - emitReady(mod); -} - -void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) -{ - try { - // Prevent file name mismatch - ver.fileName = mod->fileinfo().fileName(); - if (ver.fileName.endsWith(".disabled")) - ver.fileName.chop(9); - - QDir tmp_index_dir(m_index_dir); - - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; - - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - - update_metadata.start(); - - if (!update_metadata.isFinished()) - loop.exec(); - } - - auto metadata = Metadata::get(tmp_index_dir, pack.slug); - if (!metadata.isValid()) { - qCritical() << "Failed to generate metadata at last step!"; - emitFail(mod); - return; - } - - mod->setMetadata(metadata); - - emitReady(mod); - } catch (Json::JsonException& e) { - qDebug() << e.cause(); - - emitFail(mod); - } + emitReady(resource); } diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 14b75a1a7..3d8a8ba53 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -1,22 +1,23 @@ #pragma once #include "ModIndex.h" +#include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" +#include "minecraft/mod/Resource.h" #include "tasks/ConcurrentTask.h" -#include - class Mod; +class QDir; class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); - EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); - EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; @@ -29,35 +30,36 @@ class EnsureMetadataTask : public Task { private: // FIXME: Move to their own namespace - auto modrinthVersionsTask() -> Task::Ptr; - auto modrinthProjectsTask() -> Task::Ptr; + Task::Ptr modrinthVersionsTask(); + Task::Ptr modrinthProjectsTask(); - auto flameVersionsTask() -> Task::Ptr; - auto flameProjectsTask() -> Task::Ptr; + Task::Ptr flameVersionsTask(); + Task::Ptr flameProjectsTask(); // Helpers enum class RemoveFromList { Yes, No }; - void emitReady(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes); - void emitFail(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); + void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); // Hashes and stuff - auto createNewHash(Mod*) -> Hashing::Hasher::Ptr; - auto getExistingHash(Mod*) -> QString; + Hashing::Hasher::Ptr createNewHash(Resource*); + QString getExistingHash(Resource*); private slots: - void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); - void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); + void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource); signals: - void metadataReady(Mod*); - void metadataFailed(Mod*); + void metadataReady(Resource*); + void metadataFailed(Resource*); private: - QHash m_mods; - QDir m_index_dir; + QHash m_resources; + QDir m_indexDir; ModPlatform::ResourceProvider m_provider; - QHash m_temp_versions; + QHash m_tempVersions; Task::Ptr m_hashingTask; - Task::Ptr m_current_task; + Task::Ptr m_currentTask; + QHash m_updateMetadataTasks; }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index c3ecccf8e..7b20d37ec 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -25,13 +25,12 @@ namespace ModPlatform { -static const QMap s_indexed_version_type_names = { - { "release", IndexedVersionType::VersionType::Release }, - { "beta", IndexedVersionType::VersionType::Beta }, - { "alpha", IndexedVersionType::VersionType::Alpha } -}; +static const QMap s_indexed_version_type_names = { { "release", IndexedVersionType::Release }, + { "beta", IndexedVersionType::Beta }, + { "alpha", IndexedVersionType::Alpha } }; -static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, + Babric, BTA, LegacyFabric, Ornithe, Rift }; QList modLoaderTypesToList(ModLoaderTypes flags) { @@ -44,32 +43,14 @@ QList modLoaderTypesToList(ModLoaderTypes flags) return flagList; } -IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} - -IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) -{ - m_type = type; -} - -IndexedVersionType::IndexedVersionType(const IndexedVersionType& other) -{ - m_type = other.m_type; -} - -IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other) -{ - m_type = other.m_type; - return *this; -} - -const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type) +QString IndexedVersionType::toString() const { - return s_indexed_version_type_names.key(type, "unknown"); + return s_indexed_version_type_names.key(m_type, "unknown"); } -IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type) +IndexedVersionType IndexedVersionType::fromString(const QString& type) { - return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown); + return s_indexed_version_type_names.value(type, IndexedVersionType::Unknown); } const char* ProviderCapabilities::name(ResourceProvider p) @@ -127,6 +108,18 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString return "fabric"; case Quilt: return "quilt"; + case DataPack: + return "datapack"; + case Babric: + return "babric"; + case BTA: + return "bta-babric"; + case LegacyFabric: + return "legacy-fabric"; + case Ornithe: + return "ornithe"; + case Rift: + return "rift"; default: break; } @@ -147,7 +140,42 @@ auto getModLoaderFromString(QString type) -> ModLoaderType return Fabric; if (type == "quilt") return Quilt; + if (type == "babric") + return Babric; + if (type == "bta-babric") + return BTA; + if (type == "legacy-fabric") + return LegacyFabric; + if (type == "ornithe") + return Ornithe; + if (type == "rift") + return Rift; return {}; } +QString SideUtils::toString(Side side) +{ + switch (side) { + case Side::ClientSide: + return "client"; + case Side::ServerSide: + return "server"; + case Side::UniversalSide: + return "both"; + case Side::NoSide: + break; + } + return {}; +} + +Side SideUtils::fromString(QString side) +{ + if (side == "client") + return Side::ClientSide; + if (side == "server") + return Side::ServerSide; + if (side == "both") + return Side::UniversalSide; + return Side::UniversalSide; +} } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 8fae1bf6c..5cde2274f 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -23,23 +23,41 @@ #include #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 }; +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 +}; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK }; - enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; +enum class Side { NoSide = 0, ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; + +namespace SideUtils { +QString toString(Side side); +Side fromString(QString side); +} // namespace SideUtils + namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); @@ -58,31 +76,19 @@ struct DonationData { }; struct IndexedVersionType { - enum class VersionType { Release = 1, Beta, Alpha, Unknown }; - IndexedVersionType(const QString& type); - IndexedVersionType(const IndexedVersionType::VersionType& type); - IndexedVersionType(const IndexedVersionType& type); - IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown) {} - static const QString toString(const IndexedVersionType::VersionType& type); - static IndexedVersionType::VersionType enumFromString(const QString& type); - bool isValid() const { return m_type != IndexedVersionType::VersionType::Unknown; } - IndexedVersionType& operator=(const IndexedVersionType& other); - bool operator==(const IndexedVersionType& other) const { return m_type == other.m_type; } - bool operator==(const IndexedVersionType::VersionType& type) const { return m_type == type; } - bool operator!=(const IndexedVersionType& other) const { return m_type != other.m_type; } - bool operator!=(const IndexedVersionType::VersionType& type) const { return m_type != type; } - bool operator<(const IndexedVersionType& other) const { return m_type < other.m_type; } - bool operator<(const IndexedVersionType::VersionType& type) const { return m_type < type; } - bool operator<=(const IndexedVersionType& other) const { return m_type <= other.m_type; } - bool operator<=(const IndexedVersionType::VersionType& type) const { return m_type <= type; } - bool operator>(const IndexedVersionType& other) const { return m_type > other.m_type; } - bool operator>(const IndexedVersionType::VersionType& type) const { return m_type > type; } - bool operator>=(const IndexedVersionType& other) const { return m_type >= other.m_type; } - bool operator>=(const IndexedVersionType::VersionType& type) const { return m_type >= type; } - - QString toString() const { return toString(m_type); } - - IndexedVersionType::VersionType m_type; + enum class Enum { Unknown, Release = 1, Beta, Alpha }; + using enum Enum; + constexpr IndexedVersionType(Enum e = Unknown) : m_type(e) {} + static IndexedVersionType fromString(const QString& type); + inline 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; + explicit operator int() const { return static_cast(m_type); } + explicit operator IndexedVersionType::Enum() { return m_type; } + + private: + Enum m_type; }; struct Dependency { @@ -107,10 +113,27 @@ struct IndexedVersion { bool is_preferred = true; QString changelog; QList dependencies; - QString side; // this is for flame API + Side side; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; + + QString getVersionDisplayString() const + { + 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) { + if (version.contains(v)) { + gameVersion = ""; + break; + } + if (gameVersion.isEmpty()) { + gameVersion = QObject::tr(" for %1").arg(v); + } + } + return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type); + } }; struct ExtraPackData { @@ -138,24 +161,24 @@ struct IndexedPack { QString logoName; QString logoUrl; QString websiteUrl; - QString side; + Side side; bool versionsLoaded = false; - QVector versions; + QList versions; // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; ExtraPackData extraData; // For internal use, not provided by APIs - [[nodiscard]] bool isVersionSelected(int index) const + bool isVersionSelected(int index) const { if (!versionsLoaded) return false; return versions.at(index).is_currently_selected; } - [[nodiscard]] bool isAnyVersionSelected() const + bool isAnyVersionSelected() const { if (!versionsLoaded) return false; diff --git a/launcher/modplatform/ResourceAPI.cpp b/launcher/modplatform/ResourceAPI.cpp new file mode 100644 index 000000000..cdc1d8090 --- /dev/null +++ b/launcher/modplatform/ResourceAPI.cpp @@ -0,0 +1,300 @@ +#include "modplatform/ResourceAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/NetJob.h" + +#include "modplatform/ModIndex.h" + +#include "net/ApiDownload.h" + +Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback>&& callbacks) const +{ + auto search_url_optional = getSearchURL(args); + if (!search_url_optional.has_value()) { + callbacks.on_fail("Failed to create search URL", -1); + return nullptr; + } + + auto search_url = search_url_optional.value(); + + auto response = std::make_shared(); + auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response.get())); + + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + callbacks.on_fail(parse_error.errorString(), -1); + + return; + } + + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack::Ptr pack = std::make_shared(); + try { + loadIndexedPack(*pack, packObj); + newList << pack; + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); + continue; + } + } + + callbacks.on_succeed(newList); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + + return netJob; +} + +Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const +{ + auto versions_url_optional = getVersionsURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + auto versions_url = versions_url_optional.value(); + + auto netJob = makeShared(QString("%1::Versions").arg(args.pack->name), APPLICATION->network()); + auto response = std::make_shared(); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response.get())); + + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + QVector unsortedVersions; + try { + auto arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); + + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, args.resourceType); + if (!file.addonId.isValid()) + file.addonId = args.pack->addonId; + + 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 { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); + } + + callbacks.on_succeed(unsortedVersions); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + + return netJob; +} + +Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks) const +{ + auto response = std::make_shared(); + auto job = getProject(args.pack->addonId.toString(), response.get()); + + QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { + auto pack = args.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(); + qWarning() << *response; + return; + } + try { + auto obj = Json::requireObject(doc); + if (obj.contains("data")) + obj = Json::requireObject(obj, "data"); + loadIndexedPack(*pack, obj); + loadExtraPackInfo(*pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + callbacks.on_succeed(pack); + }); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = job.toWeakRef(); + QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto job = weak.lock()) { + if (auto netJob = qSharedPointerDynamicCast(job)) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) { + network_error_code = failed_action->replyStatusCode(); + } + } + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(job.get(), &NetJob::aborted, [callbacks] { + if (callbacks.on_abort != nullptr) + callbacks.on_abort(); + }); + return job; +} + +Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback&& callbacks) const +{ + auto versions_url_optional = getDependencyURL(args); + if (!versions_url_optional.has_value()) + return nullptr; + + 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.get())); + + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + QJsonArray arr; + if (args.dependency.version.length() != 0 && doc.isObject()) { + arr.append(doc.object()); + } else { + arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); + } + + QVector versions; + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod); + if (!file.addonId.isValid()) + file.addonId = args.dependency.addonId; + + if (file.fileId.isValid() && + (!file.loaders || args.loader & file.loaders)) // Heuristic to check if the returned value is valid + versions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion(); + callbacks.on_succeed(bestMatch); + }); + + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = netJob.toWeakRef(); + QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto netJob = weak.lock()) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) + network_error_code = failed_action->replyStatusCode(); + } + callbacks.on_fail(reason, network_error_code); + }); + return netJob; +} + +QString ResourceAPI::getGameVersionsString(std::list mcVersions) const +{ + QString s; + for (auto& ver : mcVersions) { + s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); + } + s.remove(s.length() - 1, 1); // remove last comma + return s; +} + +QString ResourceAPI::mapMCVersionToModrinth(Version v) const +{ + static const QString preString = " Pre-Release "; + auto verStr = v.toString(); + + if (verStr.contains(preString)) { + verStr.replace(preString, "-pre"); + } + verStr.replace(" ", "-"); + return verStr; +} + +Task::Ptr ResourceAPI::getProject(QString addonId, QByteArray* response) const +{ + auto project_url_optional = getInfoURL(addonId); + if (!project_url_optional.has_value()) + return nullptr; + + auto project_url = project_url_optional.value(); + + auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); + + return netJob; +} diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 17274722d..84bbe8bf0 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -4,7 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -48,6 +48,7 @@ #include "../Version.h" #include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" #include "tasks/Task.h" /* Simple class with a common interface for interacting with APIs */ @@ -66,6 +67,13 @@ class ResourceAPI { QString readable_name; }; + template + struct Callback { + std::function on_succeed; + std::function on_fail; + std::function on_abort; + }; + struct SearchArgs { ModPlatform::ResourceType type{}; int offset = 0; @@ -74,44 +82,21 @@ class ResourceAPI { std::optional sorting; std::optional loaders; std::optional> versions; - std::optional side; + std::optional side; std::optional categoryIds; - }; - struct SearchCallbacks { - std::function on_succeed; - std::function on_fail; - std::function on_abort; + bool openSource; }; struct VersionSearchArgs { - ModPlatform::IndexedPack pack; + ModPlatform::IndexedPack::Ptr pack; - std::optional > mcVersions; + std::optional> mcVersions; std::optional loaders; - - VersionSearchArgs(VersionSearchArgs const&) = default; - void operator=(VersionSearchArgs other) - { - pack = other.pack; - mcVersions = other.mcVersions; - loaders = other.loaders; - } - }; - struct VersionSearchCallbacks { - std::function on_succeed; - std::function on_fail; + ModPlatform::ResourceType resourceType; }; struct ProjectInfoArgs { - ModPlatform::IndexedPack pack; - - ProjectInfoArgs(ProjectInfoArgs const&) = default; - void operator=(ProjectInfoArgs other) { pack = other.pack; } - }; - struct ProjectInfoCallbacks { - std::function on_succeed; - std::function on_fail; - std::function on_abort; + ModPlatform::IndexedPack::Ptr pack; }; struct DependencySearchArgs { @@ -120,73 +105,52 @@ class ResourceAPI { ModPlatform::ModLoaderTypes loader; }; - struct DependencySearchCallbacks { - std::function on_succeed; - std::function on_fail; - }; - public: /** Gets a list of available sorting methods for this API. */ - [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; + virtual auto getSortingMethods() const -> QList = 0; public slots: - [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::searchProjects"; - return nullptr; - } - [[nodiscard]] virtual Task::Ptr getProject([[maybe_unused]] QString addonId, - [[maybe_unused]] std::shared_ptr response) const - { - qWarning() << "TODO: ResourceAPI::getProject"; - return nullptr; - } - [[nodiscard]] virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds, - [[maybe_unused]] std::shared_ptr response) const - { - qWarning() << "TODO: ResourceAPI::getProjects"; - return nullptr; - } - - [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::getProjectInfo"; - return nullptr; - } - [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::getProjectVersions"; - return nullptr; - } - - [[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const - { - qWarning() << "TODO"; - return nullptr; - } + virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; + + virtual Task::Ptr getProject(QString addonId, QByteArray* response) const; + virtual Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const = 0; + + virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&) const; + Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const; + virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; protected: - [[nodiscard]] inline QString debugName() const { return "External resource API"; } - - [[nodiscard]] inline QString mapMCVersionToModrinth(Version v) const - { - static const QString preString = " Pre-Release "; - auto verStr = v.toString(); - - if (verStr.contains(preString)) { - verStr.replace(preString, "-pre"); - } - verStr.replace(" ", "-"); - return verStr; - } - - [[nodiscard]] inline QString getGameVersionsString(std::list mcVersions) const - { - QString s; - for (auto& ver : mcVersions) { - s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); - } - s.remove(s.length() - 1, 1); // remove last comma - return s; - } + inline QString debugName() const { return "External resource API"; } + + QString mapMCVersionToModrinth(Version v) const; + + QString getGameVersionsString(std::list 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; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0; + virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0; + + /** Converts a JSON document to a common array format. + * + * This is needed so that different providers, with different JSON structures, can be parsed + * uniformally. You NEED to re-implement this if you intend on using the default callbacks. + */ + virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0; }; diff --git a/launcher/modplatform/ResourceType.cpp b/launcher/modplatform/ResourceType.cpp new file mode 100644 index 000000000..2758f113f --- /dev/null +++ b/launcher/modplatform/ResourceType.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ResourceType.h" + +namespace ModPlatform { +static const QMap s_packedTypeNames = { { ResourceType::ResourcePack, QObject::tr("resource pack") }, + { ResourceType::TexturePack, QObject::tr("texture pack") }, + { ResourceType::DataPack, QObject::tr("data pack") }, + { ResourceType::ShaderPack, QObject::tr("shader pack") }, + { ResourceType::World, QObject::tr("world save") }, + { ResourceType::Mod, QObject::tr("mod") }, + { ResourceType::Unknown, QObject::tr("unknown") } }; + +namespace ResourceTypeUtils { + +QString getName(ResourceType type) +{ + return s_packedTypeNames.constFind(type).value(); +} + +} // namespace ResourceTypeUtils +} // namespace ModPlatform diff --git a/launcher/modplatform/ResourceType.h b/launcher/modplatform/ResourceType.h new file mode 100644 index 000000000..390ade7ff --- /dev/null +++ b/launcher/modplatform/ResourceType.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include + +namespace ModPlatform { + +enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, World, Screenshots, TexturePack, Unknown }; + +namespace ResourceTypeUtils { +static const std::set VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, + ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; +QString getName(ResourceType type); +} // namespace ResourceTypeUtils +} // namespace ModPlatform diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp index 678db63cc..d41e446cf 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.cpp +++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -40,8 +40,9 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) loadIndexedVersion(version, versionObj); m.versions.append(version); } - m.system = Json::ensureBoolean(obj, QString("system"), false); - m.description = Json::ensureString(obj, "description", ""); + m.system = obj["system"].toBool(); + m.description = obj["description"].toString(""); - m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; } diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h index 8d18c671d..bb0434533 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -18,9 +18,9 @@ #include "ATLPackManifest.h" +#include #include #include -#include namespace ATLauncher { @@ -34,7 +34,7 @@ struct IndexedPack { int position; QString name; PackType type; - QVector versions; + QList versions; bool system; QString description; @@ -45,3 +45,4 @@ void loadIndexedPack(IndexedPack& m, QJsonObject& obj); } // namespace ATLauncher Q_DECLARE_METATYPE(ATLauncher::IndexedPack) +Q_DECLARE_METATYPE(QList) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index abe7d0177..df7a16a03 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -39,8 +39,6 @@ #include #include -#include - #include "FileSystem.h" #include "Json.h" #include "MMCZip.h" @@ -69,7 +67,8 @@ PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packNa { m_support = support; m_pack_name = packName; - m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m_pack_safe_name = packName.replace(s_regex, ""); m_version_name = version; m_install_mode = installMode; } @@ -88,11 +87,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)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response.get())); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); - QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); + connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); jobPtr = netJob; jobPtr->start(); @@ -390,7 +389,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) return m_version.loader.version; } -QString PackInstallTask::detectLibrary(VersionLibrary library) +QString PackInstallTask::detectLibrary(const VersionLibrary& library) { // Try to detect what the library is if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { @@ -424,7 +423,7 @@ QString PackInstallTask::detectLibrary(VersionLibrary library) return "org.multimc.atlauncher:" + library.md5 + ":1"; } -bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr profile) +bool PackInstallTask::createLibrariesComponent(QString instanceRoot, PackProfile* profile) { if (m_version.libraries.isEmpty()) { return true; @@ -433,14 +432,15 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared QList exempt; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); - - for (const auto& library : componentVersion->data()->libraries) { - GradleSpecifier lib(library->rawName()); - exempt.append(lib); + if (componentVersion->data()) { + for (const auto& library : componentVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } } } - { + if (minecraftVersion->data()) { for (const auto& library : minecraftVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); @@ -532,11 +532,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, target_id, f) }); return true; } -bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) +bool PackInstallTask::createPackComponent(QString instanceRoot, PackProfile* profile) { if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { return true; @@ -581,10 +581,12 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); - if (componentVersion->data()->mainClass != QString("")) { - mainClasses.append(componentVersion->data()->mainClass); + if (componentVersion->data()) { + if (componentVersion->data()->mainClass != QString("")) { + mainClasses.append(componentVersion->data()->mainClass); + } + tweakers.append(componentVersion->data()->addTweakers); } - tweakers.append(componentVersion->data()->addTweakers); } auto f = std::make_shared(); @@ -619,7 +621,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); + profile->appendComponent(ComponentPtr{ new Component(profile, target_id, f) }); return true; } @@ -641,22 +643,22 @@ void PackInstallTask::installConfigs() jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); - connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + connect(jobPtr.get(), &NetJob::succeeded, this, [this]() { abortable = false; jobPtr.reset(); extractConfigs(); }); - connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { abortable = false; jobPtr.reset(); emitFailed(reason); }); - connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); - connect(jobPtr.get(), &NetJob::aborted, [&] { + connect(jobPtr.get(), &NetJob::aborted, [this] { abortable = false; jobPtr.reset(); emitAborted(); @@ -671,22 +673,10 @@ void PackInstallTask::extractConfigs() setStatus(tr("Extracting configs...")); QDir extractDir(m_stagingPath); - - QuaZip packZip(archivePath); - if (!packZip.open(QuaZip::mdUnzip)) { - emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); - return; - } - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); -#else - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); -#endif - connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() { downloadMods(); }); - connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() { emitAborted(); }); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); m_extractFutureWatcher.setFuture(m_extractFuture); } @@ -694,7 +684,7 @@ void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); - QVector optionalMods; + QList optionalMods; for (const auto& mod : m_version.mods) { if (mod.optional) { optionalMods.push_back(mod); @@ -702,7 +692,7 @@ void PackInstallTask::downloadMods() } // Select optional mods, if pack contains any - QVector selectedMods; + QList selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); auto mods = m_support->chooseOptionalMods(m_version, optionalMods); @@ -897,13 +887,8 @@ void PackInstallTask::onModsDownloaded() jobPtr.reset(); if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); -#else - m_modExtractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); -#endif connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); @@ -948,7 +933,8 @@ bool PackInstallTask::extractMods(const QMap& toExtract, QString folderToExtract = ""; if (mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; - folderToExtract.remove(QRegularExpression("^/")); + static const QRegularExpression s_regex("^/"); + folderToExtract.remove(s_regex); } qDebug() << "Extracting " + mod.file + " to " + extractToDir; @@ -1002,10 +988,9 @@ 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()); - MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); @@ -1061,7 +1046,6 @@ void PackInstallTask::install() 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(); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index ffc358fbb..695668731 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -62,7 +62,7 @@ class UserInteractionSupport { /** * Requests a user interaction to select which optional mods should be installed. */ - virtual std::optional> chooseOptionalMods(PackVersion version, QVector mods) = 0; + virtual std::optional> chooseOptionalMods(const PackVersion& version, QList mods) = 0; /** * Requests a user interaction to select a component version from a given version list @@ -105,10 +105,10 @@ class PackInstallTask : public InstanceTask { private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); - QString detectLibrary(VersionLibrary library); + QString detectLibrary(const VersionLibrary& library); - bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); - bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + bool createLibrariesComponent(QString instanceRoot, PackProfile* profile); + bool createPackComponent(QString instanceRoot, PackProfile* profile); void deleteExistingFiles(); void installConfigs(); @@ -125,7 +125,7 @@ class PackInstallTask : public InstanceTask { bool abortable = false; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + std::unique_ptr response = std::make_unique(); InstallMode m_install_mode; QString m_pack_name; diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index 9ff2f339e..22f63ad0d 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -100,20 +100,20 @@ static ATLauncher::ModType parseModType(QString rawType) static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj) { p.type = Json::requireString(obj, "type"); - p.choose = Json::ensureBoolean(obj, QString("choose"), false); + p.choose = obj["choose"].toBool(); auto metadata = Json::requireObject(obj, "metadata"); - p.latest = Json::ensureBoolean(metadata, QString("latest"), false); - p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + p.latest = metadata["latest"].toBool(); + p.recommended = metadata["recommended"].toBool(); // Minecraft Forge - if (p.type == "forge") { - p.version = Json::ensureString(metadata, "version", ""); + if (p.type == "forge" || p.type == "neoforge") { + p.version = metadata["version"].toString(""); } // Fabric Loader if (p.type == "fabric") { - p.version = Json::ensureString(metadata, "loader", ""); + p.version = metadata["loader"].toString(""); } } @@ -126,7 +126,7 @@ static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj) p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); - p.server = Json::ensureString(obj, "server", ""); + p.server = obj["server"].toString(""); } static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj) @@ -141,7 +141,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) p.version = Json::requireString(obj, "version"); p.url = Json::requireString(obj, "url"); p.file = Json::requireString(obj, "file"); - p.md5 = Json::ensureString(obj, "md5", ""); + p.md5 = obj["md5"].toString(""); p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); @@ -161,7 +161,7 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) if (obj.contains("extractTo")) { p.extractTo_raw = Json::requireString(obj, "extractTo"); p.extractTo = parseModType(p.extractTo_raw); - p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); + p.extractFolder = obj["extractFolder"].toString("").replace("%s%", "/"); } if (obj.contains("decompType")) { @@ -170,23 +170,23 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) p.decompFile = Json::requireString(obj, "decompFile"); } - p.description = Json::ensureString(obj, QString("description"), ""); - p.optional = Json::ensureBoolean(obj, QString("optional"), false); - p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); - p.selected = Json::ensureBoolean(obj, QString("selected"), false); - p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); - p.library = Json::ensureBoolean(obj, QString("library"), false); - p.group = Json::ensureString(obj, QString("group"), ""); + p.description = obj["description"].toString(""); + p.optional = obj["optional"].toBool(); + p.recommended = obj["recommended"].toBool(); + p.selected = obj["selected"].toBool(); + p.hidden = obj["hidden"].toBool(); + p.library = obj["library"].toBool(); + p.group = obj["group"].toString(""); if (obj.contains("depends")) { auto dependsArr = Json::requireArray(obj, "depends"); for (const auto depends : dependsArr) { p.depends.append(Json::requireString(depends)); } } - p.colour = Json::ensureString(obj, QString("colour"), ""); - p.warning = Json::ensureString(obj, QString("warning"), ""); + p.colour = obj["colour"].toString(""); + p.warning = obj["warning"].toString(""); - p.client = Json::ensureBoolean(obj, QString("client"), false); + p.client = obj["client"].toBool(); // computed p.effectively_hidden = p.hidden || p.library; @@ -194,20 +194,20 @@ static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) { - m.install = Json::ensureString(obj, "install", ""); - m.update = Json::ensureString(obj, "update", ""); + m.install = obj["install"].toString(""); + m.update = obj["update"].toString(""); } static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) { - m.mainClass = Json::ensureString(obj, "mainClass", ""); - m.depends = Json::ensureString(obj, "depends", ""); + m.mainClass = obj["mainClass"].toString(""); + m.depends = obj["depends"].toString(""); } static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) { - a.arguments = Json::ensureString(obj, "arguments", ""); - a.depends = Json::ensureString(obj, "depends", ""); + a.arguments = obj["arguments"].toString(""); + a.depends = obj["depends"].toString(""); } static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj) @@ -272,7 +272,7 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj) { v.version = Json::requireString(obj, "version"); v.minecraft = Json::requireString(obj, "minecraft"); - v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); + v.noConfigs = obj["noConfigs"].toBool(); if (obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); @@ -314,22 +314,22 @@ void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj) loadVersionConfigs(v.configs, configsObj); } - auto colourObj = Json::ensureObject(obj, "colours"); + auto colourObj = obj["colours"].toObject(); for (const auto& key : colourObj.keys()) { v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } - auto warningsObj = Json::ensureObject(obj, "warnings"); + auto warningsObj = obj["warnings"].toObject(); for (const auto& key : warningsObj.keys()) { v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); } - auto messages = Json::ensureObject(obj, "messages"); + auto messages = obj["messages"].toObject(); loadVersionMessages(v.messages, messages); - auto keeps = Json::ensureObject(obj, "keeps"); + auto keeps = obj["keeps"].toObject(); loadVersionKeeps(v.keeps, keeps); - auto deletes = Json::ensureObject(obj, "deletes"); + auto deletes = obj["deletes"].toObject(); loadVersionDeletes(v.deletes, deletes); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 8db91087d..b6c3b7a84 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -36,9 +36,9 @@ #pragma once #include +#include #include #include -#include namespace ATLauncher { @@ -113,7 +113,7 @@ struct VersionMod { bool hidden; bool library; QString group; - QVector depends; + QStringList depends; QString colour; QString warning; @@ -139,8 +139,8 @@ struct VersionKeep { }; struct VersionKeeps { - QVector files; - QVector folders; + QList files; + QList folders; }; struct VersionDelete { @@ -149,8 +149,8 @@ struct VersionDelete { }; struct VersionDeletes { - QVector files; - QVector folders; + QList files; + QList folders; }; struct PackVersionMainClass { @@ -171,8 +171,8 @@ struct PackVersion { PackVersionExtraArguments extraArguments; VersionLoader loader; - QVector libraries; - QVector mods; + QList libraries; + QList mods; VersionConfigs configs; QMap colours; diff --git a/launcher/modplatform/atlauncher/ATLShareCode.h b/launcher/modplatform/atlauncher/ATLShareCode.h index 531945bce..9b56c6d7c 100644 --- a/launcher/modplatform/atlauncher/ATLShareCode.h +++ b/launcher/modplatform/atlauncher/ATLShareCode.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include namespace ATLauncher { @@ -32,7 +32,7 @@ struct ShareCodeMod { struct ShareCode { QString pack; QString version; - QVector mods; + QList mods; }; struct ShareCodeResponse { diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 8ee599f27..acffc2706 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -20,7 +20,6 @@ #include #include "Json.h" -#include "QObjectPtr.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -33,9 +32,7 @@ static const FlameAPI flameAPI; static ModrinthAPI modrinthAPI; -Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess) - : m_network(network), m_manifest(toProcess) -{} +Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { @@ -60,7 +57,7 @@ void Flame::FileResolvingTask::executeTask() for (auto file : m_manifest.files) { fileIds.push_back(QString::number(file.fileId)); } - m_task = flameAPI.getFiles(fileIds, m_result); + m_task = flameAPI.getFiles(fileIds, m_result.get()); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, step_progress]() { @@ -87,6 +84,30 @@ void Flame::FileResolvingTask::executeTask() m_task->start(); } +ModPlatform::ResourceType getResourceType(int classId) +{ + switch (classId) { + case 17: // Worlds + return ModPlatform::ResourceType::World; + case 6: // Mods + return ModPlatform::ResourceType::Mod; + case 12: // Resource Packs + // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack + /* fallthrough */ + case 4546: // Customization + // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack + /* fallthrough */ + case 4471: // Modpacks + /* fallthrough */ + case 5: // Bukkit Plugins + /* fallthrough */ + case 4559: // Addons + /* fallthrough */ + default: + return ModPlatform::ResourceType::Unknown; + } +} + void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); @@ -133,7 +154,7 @@ void Flame::FileResolvingTask::netJobFinished() return; } m_result.reset(new QByteArray()); - m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); + m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result.get()); (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, step_progress]() { @@ -206,7 +227,7 @@ void Flame::FileResolvingTask::getFlameProjects() addonIds.push_back(QString::number(file.projectId)); } - m_task = flameAPI.getProjects(addonIds, m_result); + m_task = flameAPI.getProjects(addonIds, m_result.get()); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::succeeded, this, [this, step_progress] { @@ -234,6 +255,10 @@ void Flame::FileResolvingTask::getFlameProjects() setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); FlameMod::loadIndexedPack(file->pack, entry_obj); + file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); + if (file->resourceType == ModPlatform::ResourceType::World) { + file->targetFolder = "saves"; + } } } catch (Json::JsonException& e) { qDebug() << e.cause(); diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index edd9fce9a..681d7660a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -17,8 +17,6 @@ */ #pragma once -#include - #include "PackManifest.h" #include "tasks/Task.h" @@ -26,7 +24,7 @@ namespace Flame { class FileResolvingTask : public Task { Q_OBJECT public: - explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess); + explicit FileResolvingTask(Flame::Manifest& toProcess); virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } @@ -44,9 +42,8 @@ class FileResolvingTask : public Task { void getFlameProjects(); private: /* data */ - shared_qobject_ptr m_network; Flame::Manifest m_manifest; - std::shared_ptr m_result; + std::unique_ptr m_result; Task::Ptr m_task; }; } // namespace Flame diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 699eb792a..6cf907976 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -5,6 +5,7 @@ #include "FlameAPI.h" #include #include +#include "BuildConfig.h" #include "FlameModIndex.h" #include "Application.h" @@ -14,7 +15,7 @@ #include "net/ApiUpload.h" #include "net/NetJob.h" -Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) +Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); @@ -29,7 +30,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shar QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw)); return netJob; } @@ -42,9 +43,9 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId) auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), - response)); + response.get())); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; @@ -58,7 +59,7 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId) return; } - changelog = Json::ensureString(doc.object(), "data"); + changelog = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); @@ -77,7 +78,7 @@ QString FlameAPI::getModDescription(int modId) auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)), response.get())); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -91,7 +92,7 @@ QString FlameAPI::getModDescription(int modId) return; } - description = Json::ensureString(doc.object(), "data"); + description = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); @@ -102,58 +103,7 @@ QString FlameAPI::getModDescription(int modId) return description; } -QList FlameAPI::getLatestVersions(VersionSearchArgs&& args) -{ - auto versions_url_optional = getVersionsURL(args); - if (!versions_url_optional.has_value()) - return {}; - - auto versions_url = versions_url_optional.value(); - - QEventLoop loop; - - auto netJob = makeShared(QString("Flame::GetLatestVersion(%1)").arg(args.pack.name), APPLICATION->network()); - auto response = std::make_shared(); - QList ver; - - netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - - QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - try { - auto obj = Json::requireObject(doc); - auto arr = Json::requireArray(obj, "data"); - - for (auto file : arr) { - auto file_obj = Json::requireObject(file); - ver.append(FlameMod::loadIndexedPackVersion(file_obj)); - } - - } catch (Json::JsonException& e) { - qCritical() << "Failed to parse response from a version request."; - qCritical() << e.what(); - qDebug() << doc; - } - }); - - QObject::connect(netJob.get(), &NetJob::finished, &loop, &QEventLoop::quit); - - netJob->start(); - - loop.exec(); - - return ver; -} - -Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +Task::Ptr FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); @@ -168,14 +118,14 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } -Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const +Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, QByteArray* response) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); @@ -190,18 +140,18 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } -Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const +Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, QByteArray* response) const { auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); netJob->addNetAction( - Net::ApiDownload::makeByteArray(QUrl(QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(addonId, fileId)), response)); + Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)), response)); QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); @@ -221,21 +171,21 @@ QList FlameAPI::getSortingMethods() const { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } -Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) +Task::Ptr FlameAPI::getCategories(QByteArray* response, ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QUrl(QString("https://api.curseforge.com/v1/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); + QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return netJob; } -Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +Task::Ptr FlameAPI::getModCategories(QByteArray* response) { - return getCategories(response, ModPlatform::ResourceType::MOD); + return getCategories(response, ModPlatform::ResourceType::Mod); } -QList FlameAPI::loadModCategories(std::shared_ptr response) +QList FlameAPI::loadModCategories(QByteArray* response) { QList categories; QJsonParseError parse_error{}; @@ -268,9 +218,19 @@ QList FlameAPI::loadModCategories(std::shared_ptr FlameAPI::getLatestVersion(QList versions, QList instanceLoaders, - ModPlatform::ModLoaderTypes modLoaders) + ModPlatform::ModLoaderTypes modLoaders, + bool checkLoaders) { static const auto noLoader = ModPlatform::ModLoaderType(0); + if (!checkLoaders) { + std::optional ver; + for (auto file_tmp : versions) { + if (!ver.has_value() || file_tmp.date > ver->date) { + ver = file_tmp; + } + } + return ver; + } QHash bestMatch; auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { if (bestMatch.contains(loader)) { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 3ca0d5448..a5f2146c0 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -6,30 +6,33 @@ #include #include +#include "BuildConfig.h" +#include "Json.h" +#include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" -#include "modplatform/helpers/NetworkResourceAPI.h" +#include "modplatform/flame/FlameModIndex.h" -class FlameAPI : public NetworkResourceAPI { +class FlameAPI : public ResourceAPI { public: QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); - QList getLatestVersions(VersionSearchArgs&& args); std::optional getLatestVersion(QList versions, QList instanceLoaders, - ModPlatform::ModLoaderTypes fallback); + 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; + Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; + Task::Ptr matchFingerprints(const QList& fingerprints, QByteArray* response); + Task::Ptr getFiles(const QStringList& fileIds, QByteArray* response) const; + Task::Ptr getFile(const QString& addonId, const QString& fileId, QByteArray* response) 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 Task::Ptr getCategories(QByteArray* response, ModPlatform::ResourceType type); + static Task::Ptr getModCategories(QByteArray* response); + static QList loadModCategories(QByteArray* response); - [[nodiscard]] QList getSortingMethods() const override; + QList getSortingMethods() const override; static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) { @@ -41,14 +44,16 @@ class FlameAPI : public NetworkResourceAPI { { switch (type) { default: - case ModPlatform::ResourceType::MOD: + case ModPlatform::ResourceType::Mod: return 6; - case ModPlatform::ResourceType::RESOURCE_PACK: + case ModPlatform::ResourceType::ResourcePack: return 12; - case ModPlatform::ResourceType::SHADER_PACK: + case ModPlatform::ResourceType::ShaderPack: return 6552; - case ModPlatform::ResourceType::MODPACK: + case ModPlatform::ResourceType::Modpack: return 4471; + case ModPlatform::ResourceType::DataPack: + return 6945; } } @@ -68,6 +73,13 @@ class FlameAPI : public NetworkResourceAPI { return 5; case ModPlatform::NeoForge: return 6; + case ModPlatform::DataPack: + case ModPlatform::Babric: + case ModPlatform::BTA: + case ModPlatform::LegacyFabric: + case ModPlatform::Ornithe: + case ModPlatform::Rift: + break; // not supported } return 0; } @@ -86,7 +98,7 @@ class FlameAPI : public NetworkResourceAPI { static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } public: - [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override + std::optional getSearchURL(SearchArgs const& args) const override { QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); @@ -97,43 +109,63 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value() && args.loaders.value() != 0) - get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); + if (args.loaders.has_value()) { + ModPlatform::ModLoaderTypes loaders = args.loaders.value(); + loaders &= ~ModPlatform::ModLoaderType::DataPack; + if (loaders != 0) + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); + } if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); if (args.versions.has_value() && !args.versions.value().empty()) get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); - return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); - } - - private: - [[nodiscard]] std::optional getInfoURL(QString const& id) const override - { - return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } - [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override + std::optional getVersionsURL(VersionSearchArgs const& args) const override { - auto addonId = args.pack.addonId.toString(); - QString url = QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000").arg(addonId); + auto addonId = args.pack->addonId.toString(); + QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value() && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && + ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; } - [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object()["data"].toArray(); } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); } + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override + { + auto arr = FlameMod::loadIndexedPackVersion(obj); + if (resourceType != ModPlatform::ResourceType::TexturePack) { + return arr; + } + // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. + auto const& 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"); })) { + return arr; + } + return {}; + }; + 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 { auto addonId = args.dependency.addonId.toString(); auto url = - QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); url += QString("&modLoaderType=%1").arg(mappedModLoader); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 370f37c5f..58dd0ce33 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -3,180 +3,201 @@ #include "FlameAPI.h" #include "FlameModIndex.h" -#include +#include #include #include "Json.h" +#include "QObjectPtr.h" #include "ResourceDownloadTask.h" -#include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" +#include "modplatform/ModIndex.h" #include "net/ApiDownload.h" +#include "net/NetJob.h" +#include "tasks/Task.h" static FlameAPI api; bool FlameCheckUpdate::abort() { - m_was_aborted = true; - if (m_net_job) - return m_net_job->abort(); - return true; + bool result = false; + if (m_task && m_task->canAbort()) { + result = m_task->abort(); + } + Task::abort(); + return result; } -ModPlatform::IndexedPack FlameCheckUpdate::getProjectInfo(ModPlatform::IndexedVersion& ver_info) +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void FlameCheckUpdate::executeTask() { - ModPlatform::IndexedPack pack; + setStatus(tr("Preparing resources for CurseForge...")); + + auto netJob = new NetJob("Get latest versions", APPLICATION->network()); + connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); + + connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); + for (auto* resource : m_resources) { + auto project = std::make_shared(); + project->addonId = resource->metadata()->project_id.toString(); + auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions }); + if (!versionsUrlOptional.has_value()) + continue; - QEventLoop loop; + auto response = std::make_shared(); + auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response.get()); - auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); + connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response.get()); }); + netJob->addNetAction(task); + } + m_task.reset(netJob); + m_task->start(); +} - auto response = std::make_shared(); - auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); - auto dl = Net::ApiDownload::makeByteArray(url, response); - get_project_job->addNetAction(dl); +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(); + qWarning() << *response; + return; + } - QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } + // Fake pack with the necessary info to pass to the download task :) + auto pack = std::make_shared(); + pack->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; + pack->provider = ModPlatform::ResourceProvider::FLAME; + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + FlameMod::loadIndexedPackVersions(*pack.get(), arr); + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); - try { - auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::requireObject(doc_obj, "data"); - FlameMod::loadIndexedPack(pack, data_obj); - } catch (Json::JsonException& e) { - qWarning() << e.cause(); - qDebug() << doc; - } - }); + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); - connect(get_project_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed); - QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] { - get_project_job->deleteLater(); - loop.quit(); - }); + if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { + QString reason; + if (dynamic_cast(resource) != nullptr) + reason = + tr("No valid version found for this resource. It's probably unavailable for the current game " + "version / mod loader."); + else + reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); - get_project_job->start(); - loop.exec(); + emit checkFailed(resource, reason); + return; + } - return pack; -} + if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { + m_blocked[resource] = latest_ver->fileId.toString(); + return; + } -ModPlatform::IndexedVersion FlameCheckUpdate::getFileInfo(int addonId, int fileId) -{ - ModPlatform::IndexedVersion ver; + 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"); + } - QEventLoop loop; + 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()); + } + m_deps.append(std::make_shared(pack, latest_ver.value())); +} - auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); +void FlameCheckUpdate::collectBlockedMods() +{ + QStringList addonIds; + QHash quickSearch; + for (auto const& resource : m_blocked.keys()) { + auto addonId = resource->metadata()->project_id.toString(); + addonIds.append(addonId); + quickSearch[addonId] = resource; + } auto response = std::make_shared(); - auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); - auto dl = Net::ApiDownload::makeByteArray(url, response); - get_file_info_job->addNetAction(dl); + Task::Ptr projTask; + + if (addonIds.isEmpty()) { + emitSucceeded(); + return; + } else if (addonIds.size() == 1) { + projTask = api.getProject(*addonIds.begin(), response.get()); + } else { + projTask = api.getProjects(addonIds, response.get()); + } - QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() { + connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { QJsonParseError parse_error{}; - QJsonDocument 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 FlameCheckUpdate at " << parse_error.offset + qWarning() << "Error while parsing JSON response from Flame projects task at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; } try { - auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::requireObject(doc_obj, "data"); - ver = FlameMod::loadIndexedPackVersion(data_obj); + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + + auto resource = quickSearch.find(id).value(); + + ModPlatform::IndexedPack pack; + try { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); + + FlameMod::loadIndexedPack(pack, entry_obj); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); + emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), + recover_url); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } } catch (Json::JsonException& e) { - qWarning() << e.cause(); + qDebug() << e.cause(); qDebug() << doc; } }); - connect(get_file_info_job, &NetJob::failed, this, &FlameCheckUpdate::emitFailed); - QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] { - get_file_info_job->deleteLater(); - loop.quit(); - }); - - get_file_info_job->start(); - loop.exec(); - - return ver; -} - -/* Check for update: - * - Get latest version available - * - Compare hash of the latest version with the current hash - * - If equal, no updates, else, there's updates, so add to the list - * */ -void FlameCheckUpdate::executeTask() -{ - setStatus(tr("Preparing mods for CurseForge...")); - - int i = 0; - for (auto* mod : m_mods) { - setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name())); - setProgress(i++, m_mods.size()); - - auto latest_vers = api.getLatestVersions({ { mod->metadata()->project_id.toString() }, m_game_versions }); - - // Check if we were aborted while getting the latest version - if (m_was_aborted) { - aborted(); - return; - } - auto latest_ver = api.getLatestVersion(latest_vers, m_loaders_list, mod->loaders()); - - setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name())); - - if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { - emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " - "version / mod loader.")); - continue; - } - - if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != mod->metadata()->file_id) { - auto pack = getProjectInfo(latest_ver.value()); - auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver->fileId.toString()); - emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url); - - continue; - } - - // Fake pack with the necessary info to pass to the download task :) - auto pack = std::make_shared(); - pack->name = mod->name(); - pack->slug = mod->metadata()->slug; - pack->addonId = mod->metadata()->project_id; - pack->websiteUrl = mod->homeurl(); - for (auto& author : mod->authors()) - pack->authors.append({ author }); - pack->description = mod->description(); - pack->provider = ModPlatform::ResourceProvider::FLAME; - if (!latest_ver->hash.isEmpty() && (mod->metadata()->hash != latest_ver->hash || mod->status() == ModStatus::NotInstalled)) { - auto old_version = mod->version(); - if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { - auto current_ver = getFileInfo(latest_ver->addonId.toInt(), mod->metadata()->file_id.toInt()); - old_version = current_ver.version; - } - - auto download_task = makeShared(pack, latest_ver.value(), m_mods_folder); - m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, - api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), - ModPlatform::ResourceProvider::FLAME, download_task, mod->enabled()); - } - m_deps.append(std::make_shared(pack, latest_ver.value())); - } - emitSucceeded(); + connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error + connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress); + connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); + connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); + m_task.reset(projTask); + m_task->start(); } diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index e30ae35b9..d4b97a2b7 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -1,17 +1,16 @@ #pragma once #include "modplatform/CheckUpdateTask.h" -#include "net/NetJob.h" class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(QList& mods, + FlameCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loadersList, mods_folder) + ResourceFolderModel* resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), resourceModel) {} public slots: @@ -19,12 +18,12 @@ class FlameCheckUpdate : public CheckUpdateTask { protected slots: void executeTask() override; + private slots: + void getLatestVersionCallback(Resource* resource, QByteArray* response); + void collectBlockedMods(); private: - ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info); - ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId); + Task::Ptr m_task = nullptr; - NetJob* m_net_job = nullptr; - - bool m_was_aborted = false; + QHash m_blocked; }; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index d6eb45ef5..967eab8d3 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -36,7 +36,7 @@ #include "FlameInstanceCreationTask.h" #include "QObjectPtr.h" -#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" @@ -54,6 +54,7 @@ #include "settings/INISettingsObject.h" +#include "sys.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -75,12 +76,12 @@ bool FlameCreationTask::abort() return false; m_abort = true; - if (m_process_update_file_info_job) - m_process_update_file_info_job->abort(); - if (m_files_job) - m_files_job->abort(); - if (m_mod_id_resolver) - m_mod_id_resolver->abort(); + if (m_processUpdateFileInfoJob) + m_processUpdateFileInfoJob->abort(); + if (m_filesJob) + m_filesJob->abort(); + if (m_modIdResolver) + m_modIdResolver->abort(); return Task::abort(); } @@ -90,7 +91,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); @@ -184,7 +185,7 @@ bool FlameCreationTask::updateInstance() } auto raw_response = std::make_shared(); - auto job = api.getFiles(fileIds, raw_response); + auto job = api.getFiles(fileIds, raw_response.get()); QEventLoop loop; @@ -227,17 +228,22 @@ bool FlameCreationTask::updateInstance() QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); qDebug() << "Scheduling" << relative_path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); + if (relative_path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9))); + } else { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled")); + } } }); connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; }); connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); - m_process_update_file_info_job = job; + m_processUpdateFileInfoJob = job; job->start(); loop.exec(); - m_process_update_file_info_job = nullptr; + m_processUpdateFileInfoJob = nullptr; } else { // We don't have an old index file, so we may duplicate stuff! auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), @@ -380,13 +386,14 @@ 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); + MinecraftInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto mcVersion = m_pack.minecraft.version; // Hack to correct some 'special sauce'... if (mcVersion.endsWith('.')) { - mcVersion.remove(QRegularExpression("[.]+$")); + static const QRegularExpression s_regex("[.]+$"); + mcVersion.remove(s_regex); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } @@ -412,6 +419,24 @@ bool FlameCreationTask::createInstance() } } + 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; + const uint64_t max = sysMiB * 0.9; + + if (static_cast(recommendedRAM) > max) { + logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!") + .arg(recommendedRAM) + .arg(max)); + recommendedRAM = max; + } + + instance.settings()->set("OverrideMemory", true); + instance.settings()->set("MaxMemAlloc", recommendedRAM); + } + QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); QFileInfo jarmodsInfo(jarmodsPath); if (jarmodsInfo.isDir()) { @@ -430,26 +455,26 @@ bool FlameCreationTask::createInstance() } // Don't add managed info to packs without an ID (most likely imported from ZIP) - if (!m_managed_id.isEmpty()) - instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); + if (!m_managedId.isEmpty()) + instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); else instance.setManagedPack("flame", "", name(), "", ""); instance.setName(name()); - m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack)); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { - m_mod_id_resolver.reset(); + m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack)); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { + m_modIdResolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); loop.quit(); }); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); - m_mod_id_resolver->start(); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); + connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); + m_modIdResolver->start(); loop.exec(); @@ -468,16 +493,35 @@ bool FlameCreationTask::createInstance() void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) { - auto results = m_mod_id_resolver->getResults(); + auto results = m_modIdResolver->getResults().files; + + QStringList optionalFiles; + for (auto& result : results) { + if (!result.required) { + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); + } + } + + if (!optionalFiles.empty()) { + OptionalModDialog optionalModDialog(m_parent, optionalFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + loop.quit(); + return; + } + + m_selectedOptionalMods = optionalModDialog.getResult(); + } // first check for blocked mods QList blocked_mods; auto anyBlocked = false; - for (const auto& result : results.files.values()) { - if (result.version.fileName.endsWith(".zip")) { - m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder)); + for (const auto& result : results.values()) { + if (result.resourceType != ModPlatform::ResourceType::Mod) { + m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } + // skip optional mods that were not selected if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = result.version.fileName; @@ -486,6 +530,10 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; + auto fileName = result.version.fileName; + fileName = FS::RemoveInvalidPathChars(fileName); + auto relpath = FS::PathCombine(result.targetFolder, fileName); + blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath); blocked_mods.append(blocked_mod); @@ -507,7 +555,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { - m_mod_id_resolver.reset(); + m_modIdResolver.reset(); setError("Canceled"); loop.quit(); } @@ -518,33 +566,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { - m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); - auto results = m_mod_id_resolver->getResults().files; + m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); + auto results = m_modIdResolver->getResults().files; - QStringList optionalFiles; - for (auto& result : results) { - if (!result.required) { - optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); - } - } - - QStringList selectedOptionalMods; - if (!optionalFiles.empty()) { - OptionalModDialog optionalModDialog(m_parent, optionalFiles); - if (optionalModDialog.exec() == QDialog::Rejected) { - emitAborted(); - loop.quit(); - return; - } - - selectedOptionalMods = optionalModDialog.getResult(); - } for (const auto& result : results) { auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); - if (!result.required && !selectedOptionalMods.contains(relpath)) { + if (!result.required && !m_selectedOptionalMods.contains(relpath)) { relpath += ".disabled"; } @@ -554,26 +584,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) if (!result.version.downloadUrl.isEmpty()) { qDebug() << "Will download" << result.version.downloadUrl << "to" << path; auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); - m_files_job->addNetAction(dl); + m_filesJob->addNetAction(dl); } } - connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() { - m_files_job.reset(); - validateZIPResources(loop); + connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() { + m_filesJob.reset(); + validateOtherResources(loop); }); - connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { - m_files_job.reset(); + connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) { + m_filesJob.reset(); setError(reason); }); - connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { + connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); - connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); + connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); - m_files_job->start(); + m_filesJob->start(); } /// @brief copy the matched blocked mods to the instance staging area @@ -592,13 +622,21 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) } auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); + if (mod.disabled) + destPath += ".disabled"; 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" << destPath; - if (!FS::copy(mod.localPath, destPath)()) { - qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + if (mod.move) { + if (!FS::move(mod.localPath, destPath)) { + qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed"; + } + } else { + if (!FS::copy(mod.localPath, destPath)()) { + qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; + } } i++; @@ -608,11 +646,11 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResources(QEventLoop& loop) +void FlameCreationTask::validateOtherResources(QEventLoop& loop) { - qDebug() << "Validating whether resources stored as .zip are in the right place"; + qDebug() << "Validating whether other resources are in the right place"; QStringList zipMods; - for (auto [fileName, targetFolder] : m_ZIP_resources) { + for (auto [fileName, targetFolder] : m_otherResources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); @@ -649,44 +687,46 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop) QString worldPath; switch (type) { - case PackedResourceType::Mod: + case ModPlatform::ResourceType::Mod: validatePath(fileName, targetFolder, "mods"); zipMods.push_back(fileName); break; - case PackedResourceType::ResourcePack: + case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break; - case PackedResourceType::TexturePack: + case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break; - case PackedResourceType::DataPack: + case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break; - case PackedResourceType::ShaderPack: + case ModPlatform::ResourceType::ShaderPack: // in theory flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occur in the future validatePath(fileName, targetFolder, "shaderpacks"); break; - case PackedResourceType::WorldSave: + case ModPlatform::ResourceType::World: worldPath = validatePath(fileName, targetFolder, "saves"); installWorld(worldPath); break; - case PackedResourceType::UNKNOWN: + case ModPlatform::ResourceType::Unknown: + /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; break; } } + // TODO make this work with other sorts of resource auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); - auto results = m_mod_id_resolver->getResults().files; + auto results = m_modIdResolver->getResults().files; auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); for (auto file : results) { if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { continue; } - task->addTask(makeShared(folder, file.pack, file.version)); + task->addTask(makeShared(folder, file.pack, file.version)); } connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); - m_process_update_file_info_job = task; + m_processUpdateFileInfoJob = task; task->start(); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 28ab176c2..2fe81a909 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -52,12 +52,12 @@ class FlameCreationTask final : public InstanceCreationTask { public: FlameCreationTask(const QString& staging_path, - SettingsObjectPtr global_settings, + SettingsObject* global_settings, 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)) + : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); @@ -74,22 +74,24 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); - void validateZIPResources(QEventLoop& loop); + void validateOtherResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: QWidget* m_parent = nullptr; - shared_qobject_ptr m_mod_id_resolver; + shared_qobject_ptr m_modIdResolver; Flame::Manifest m_pack; // Handle to allow aborting - Task::Ptr m_process_update_file_info_job = nullptr; - NetJob::Ptr m_files_job = nullptr; + Task::Ptr m_processUpdateFileInfoJob = nullptr; + NetJob::Ptr m_filesJob = nullptr; - QString m_managed_id, m_managed_version_id; + QString m_managedId, m_managedVersionId; - QList> m_ZIP_resources; + QList> m_otherResources; - std::optional m_instance; + std::optional m_instance; + + QStringList m_selectedOptionalMods; }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index eb5b3cc67..3b0f7f6ae 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -4,6 +4,7 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" static FlameAPI api; @@ -14,23 +15,26 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.provider = ModPlatform::ResourceProvider::FLAME; pack.name = Json::requireString(obj, "name"); pack.slug = Json::requireString(obj, "slug"); - pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); - pack.description = Json::ensureString(obj, "summary", ""); + pack.websiteUrl = obj["links"].toObject()["websiteUrl"].toString(""); + pack.description = obj["summary"].toString(""); - QJsonObject logo = Json::ensureObject(obj, "logo"); - pack.logoName = Json::ensureString(logo, "title"); - pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); + QJsonObject logo = obj["logo"].toObject(); + pack.logoName = logo["title"].toString(); + pack.logoUrl = logo["thumbnailUrl"].toString(); if (pack.logoUrl.isEmpty()) { - pack.logoUrl = Json::ensureString(logo, "url"); + pack.logoUrl = logo["url"].toString(); } - auto authors = Json::ensureArray(obj, "authors"); - for (auto authorIter : authors) { - auto author = Json::requireObject(authorIter); - ModPlatform::ModpackAuthor packAuthor; - packAuthor.name = Json::requireString(author, "name"); - packAuthor.url = Json::requireString(author, "url"); - pack.authors.append(packAuthor); + auto authors = obj["authors"].toArray(); + if (!authors.isEmpty()) { + pack.authors.clear(); + for (auto authorIter : authors) { + auto author = Json::requireObject(authorIter); + ModPlatform::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } } pack.extraDataLoaded = false; @@ -39,17 +43,17 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - auto links_obj = Json::ensureObject(obj, "links"); + auto links_obj = obj["links"].toObject(); - pack.extraData.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); + pack.extraData.issuesUrl = links_obj["issuesUrl"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); - pack.extraData.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); + pack.extraData.sourceUrl = links_obj["sourceUrl"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); - pack.extraData.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); + pack.extraData.wikiUrl = links_obj["wikiUrl"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); @@ -57,7 +61,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.extraDataLoaded = true; } -void FlameMod::loadBody(ModPlatform::IndexedPack& pack, [[maybe_unused]] QJsonObject& obj) +void FlameMod::loadBody(ModPlatform::IndexedPack& pack) { pack.extraData.body = api.getModDescription(pack.addonId.toInt()); @@ -76,12 +80,9 @@ static QString enumToString(int hash_algorithm) } } -void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - [[maybe_unused]] const shared_qobject_ptr& network, - const BaseInstance* inst) +void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -113,6 +114,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> if (str.contains('.')) file.mcVersion.append(str); + file.side = ModPlatform::Side::NoSide; if (auto loader = str.toLower(); loader == "neoforge") file.loaders |= ModPlatform::NeoForge; else if (loader == "forge") @@ -126,10 +128,10 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; else if (loader == "server" || loader == "client") { - if (file.side.isEmpty()) - file.side = loader; - else if (file.side != loader) - file.side = "both"; + if (file.side == ModPlatform::Side::NoSide) + file.side = ModPlatform::SideUtils::fromString(loader); + else if (file.side != ModPlatform::SideUtils::fromString(loader)) + file.side = ModPlatform::Side::UniversalSide; } } @@ -137,31 +139,32 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::ensureString(obj, "downloadUrl"); + file.downloadUrl = obj["downloadUrl"].toString(); file.fileName = Json::requireString(obj, "fileName"); file.fileName = FS::RemoveInvalidPathChars(file.fileName); - ModPlatform::IndexedVersionType::VersionType ver_type; + ModPlatform::IndexedVersionType ver_type; switch (Json::requireInteger(obj, "releaseType")) { case 1: - ver_type = ModPlatform::IndexedVersionType::VersionType::Release; + ver_type = ModPlatform::IndexedVersionType::Release; break; case 2: - ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; + ver_type = ModPlatform::IndexedVersionType::Beta; break; case 3: - ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; + ver_type = ModPlatform::IndexedVersionType::Alpha; break; default: - ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + ver_type = ModPlatform::IndexedVersionType::Unknown; + break; } - file.version_type = ModPlatform::IndexedVersionType(ver_type); + file.version_type = ver_type; - auto hash_list = Json::ensureArray(obj, "hashes"); + auto hash_list = obj["hashes"].toArray(); for (auto h : hash_list) { - auto hash_entry = Json::ensureObject(h); + auto hash_entry = h.toObject(); auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); - auto hash_algo = enumToString(Json::ensureInteger(hash_entry, "algo", 1, "algorithm")); + auto hash_algo = enumToString(hash_entry["algo"].toInt(1)); if (hash_types.contains(hash_algo)) { file.hash = Json::requireString(hash_entry, "value"); file.hash_type = hash_algo; @@ -169,9 +172,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> } } - auto dependencies = Json::ensureArray(obj, "dependencies"); + auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { - auto dep = Json::ensureObject(d); + auto dep = d.toObject(); ModPlatform::Dependency dependency; dependency.addonId = Json::requireInteger(dep, "modId"); switch (Json::requireInteger(dep, "relationType")) { @@ -205,31 +208,3 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> return file; } - -ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -{ - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - QVector versions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - - auto file = loadIndexedPackVersion(obj); - if (!file.addonId.isValid()) - file.addonId = m.addonId; - - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid - versions.append(file); - } - - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(versions.begin(), versions.end(), orderSortPredicate); - if (versions.size() != 0) - return versions.front(); - return {}; -} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 1bcaa44ba..2631fab74 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -6,18 +6,13 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - const shared_qobject_ptr& network, - const BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; -} // namespace FlameMod \ No newline at end of file +void loadBody(ModPlatform::IndexedPack& m); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); +ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); +} // namespace FlameMod diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 4a1b2bd87..b1eba63a0 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -30,7 +30,6 @@ #include #include "Application.h" #include "Json.h" -#include "MMCZip.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" @@ -38,25 +37,13 @@ #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" +#include "archive/ExportToZipTask.h" + const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); -FlamePackExportTask::FlamePackExportTask(const QString& name, - const QString& version, - const QString& author, - bool optionalFiles, - InstancePtr instance, - const QString& output, - MMCZip::FilterFunction filter) - : name(name) - , version(version) - , author(author) - , optionalFiles(optionalFiles) - , instance(instance) - , mcInstance(dynamic_cast(instance.get())) - , gameRoot(instance->gameRoot()) - , output(output) - , filter(filter) +FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options) + : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot()) {} void FlamePackExportTask::executeTask() @@ -70,7 +57,6 @@ bool FlamePackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -82,7 +68,7 @@ void FlamePackExportTask::collectFiles() QCoreApplication::processEvents(); files.clear(); - if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { + if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) { emitFailed(tr("Could not search for files")); return; } @@ -90,11 +76,8 @@ void FlamePackExportTask::collectFiles() pendingHashes.clear(); resolvedFiles.clear(); - if (mcInstance != nullptr) { - mcInstance->loaderModList()->update(); - connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); - } else - collectHashes(); + m_options.instance->loaderModList()->update(); + connect(m_options.instance->loaderModList(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); } void FlamePackExportTask::collectHashes() @@ -102,11 +85,11 @@ void FlamePackExportTask::collectHashes() setAbortable(true); setStatus(tr("Finding file hashes...")); setProgress(1, 5); - auto allMods = mcInstance->loaderModList()->allMods(); + 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) { - const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + 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) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); @@ -171,6 +154,7 @@ void FlamePackExportTask::collectHashes() progressStep->status = status; stepProgress(*progressStep); }); + connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); hashingTask->start(); } @@ -190,7 +174,7 @@ void FlamePackExportTask::makeApiRequest() fingerprints.push_back(murmur.toUInt()); } - task.reset(api.matchFingerprints(fingerprints, response)); + task.reset(api.matchFingerprints(fingerprints, response.get())); connect(task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parseError{}; @@ -216,8 +200,8 @@ void FlamePackExportTask::makeApiRequest() return; } for (auto match : dataArr) { - auto matchObj = Json::ensureObject(match, {}); - auto fileObj = Json::ensureObject(matchObj, "file", {}); + auto matchObj = match.toObject(); + auto fileObj = matchObj["file"].toObject(); if (matchObj.isEmpty() || fileObj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; @@ -225,7 +209,7 @@ void FlamePackExportTask::makeApiRequest() return; } - auto fingerprint = QString::number(Json::ensureVariant(fileObj, "fileFingerprint").toUInt()); + auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger()); auto mod = pendingHashes.find(fingerprint); if (mod == pendingHashes.end()) { qWarning() << "Invalid fingerprint from the API response."; @@ -233,7 +217,7 @@ void FlamePackExportTask::makeApiRequest() } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); - if (Json::ensureBoolean(fileObj, QStringLiteral("isAvailable"), false)) + if (fileObj["isAvailable"].toBool()) resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"), mod->enabled, mod->isMod }); } @@ -246,6 +230,7 @@ void FlamePackExportTask::makeApiRequest() getProjectsInfo(); }); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task->start(); } @@ -267,9 +252,9 @@ void FlamePackExportTask::getProjectsInfo() buildZip(); return; } else if (addonIds.size() == 1) { - projTask = api.getProject(*addonIds.begin(), response); + projTask = api.getProject(*addonIds.begin(), response.get()); } else { - projTask = api.getProjects(addonIds, response); + projTask = api.getProjects(addonIds, response.get()); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] { @@ -324,6 +309,7 @@ void FlamePackExportTask::getProjectsInfo() buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } @@ -333,13 +319,13 @@ void FlamePackExportTask::buildZip() setStatus(tr("Adding files...")); setProgress(4, 5); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, false); + auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); QStringList exclude; std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude), - [this](QString file) { return gameRoot.relativeFilePath(file); }); + [this](QString file) { return m_gameRoot.relativeFilePath(file); }); zipTask->setExcludeFiles(exclude); auto progressStep = std::make_shared(); @@ -374,52 +360,56 @@ QByteArray FlamePackExportTask::generateIndex() QJsonObject obj; obj["manifestType"] = "minecraftModpack"; obj["manifestVersion"] = 1; - obj["name"] = name; - obj["version"] = version; - obj["author"] = author; + obj["name"] = m_options.name; + obj["version"] = m_options.version; + obj["author"] = m_options.author; obj["overrides"] = "overrides"; - if (mcInstance) { - QJsonObject version; - auto profile = mcInstance->getPackProfile(); - // collect all supported components - const ComponentPtr minecraft = profile->getComponent("net.minecraft"); - const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); - const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); - const ComponentPtr forge = profile->getComponent("net.minecraftforge"); - const ComponentPtr neoforge = profile->getComponent("net.neoforged"); - - // convert all available components to mrpack dependencies - if (minecraft != nullptr) - version["version"] = minecraft->m_version; - QString id; - if (quilt != nullptr) - id = "quilt-" + quilt->m_version; - else if (fabric != nullptr) - id = "fabric-" + fabric->m_version; - else if (forge != nullptr) - id = "forge-" + forge->m_version; - else if (neoforge != nullptr) { - id = "neoforge-"; - if (minecraft->m_version == "1.20.1") - id += "1.20.1-"; - id += neoforge->m_version; - } - version["modLoaders"] = QJsonArray(); - if (!id.isEmpty()) { - QJsonObject loader; - loader["id"] = id; - loader["primary"] = true; - version["modLoaders"] = QJsonArray({ loader }); - } - obj["minecraft"] = version; + + QJsonObject version; + + auto profile = m_options.instance->getPackProfile(); + // collect all supported components + const ComponentPtr minecraft = profile->getComponent("net.minecraft"); + const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); + const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); + const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoforge = profile->getComponent("net.neoforged"); + + // convert all available components to mrpack dependencies + if (minecraft != nullptr) + version["version"] = minecraft->m_version; + QString id; + if (quilt != nullptr) + id = "quilt-" + quilt->m_version; + else if (fabric != nullptr) + id = "fabric-" + fabric->m_version; + else if (forge != nullptr) + id = "forge-" + forge->m_version; + else if (neoforge != nullptr) { + id = "neoforge-"; + if (minecraft->m_version == "1.20.1") + id += "1.20.1-"; + id += neoforge->m_version; } + version["modLoaders"] = QJsonArray(); + if (!id.isEmpty()) { + QJsonObject loader; + loader["id"] = id; + loader["primary"] = true; + version["modLoaders"] = QJsonArray({ loader }); + } + + if (m_options.recommendedRAM > 0) + version["recommendedRam"] = m_options.recommendedRAM; + + obj["minecraft"] = version; QJsonArray files; for (auto mod : resolvedFiles) { QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; - file["required"] = mod.enabled || !optionalFiles; + file["required"] = mod.enabled || !m_options.optionalFiles; files << file; } obj["files"] = files; @@ -440,4 +430,4 @@ QByteArray FlamePackExportTask::generateHTML() } content = "
      " + content + "
    "; return content.toUtf8(); -} \ No newline at end of file +} diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index b11eb17fa..f47041704 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -19,22 +19,26 @@ #pragma once -#include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FlameAPI.h" #include "tasks/Task.h" +struct FlamePackExportOptions { + QString name; + QString version; + QString author; + bool optionalFiles; + MinecraftInstance* instance; + QString output; + MMCZip::FilterFileFunction filter; + int recommendedRAM; +}; + class FlamePackExportTask : public Task { Q_OBJECT public: - FlamePackExportTask(const QString& name, - const QString& version, - const QString& author, - bool optionalFiles, - InstancePtr instance, - const QString& output, - MMCZip::FilterFunction filter); + FlamePackExportTask(FlamePackExportOptions&& options); protected: void executeTask() override; @@ -45,13 +49,6 @@ class FlamePackExportTask : public Task { static const QStringList FILE_EXTENSIONS; // inputs - const QString name, version, author; - const bool optionalFiles; - const InstancePtr instance; - MinecraftInstance* mcInstance; - const QDir gameRoot; - const QString output; - const MMCZip::FilterFunction filter; struct ResolvedFile { int addonId; @@ -70,6 +67,9 @@ class FlamePackExportTask : public Task { bool isMod; }; + FlamePackExportOptions m_options; + QDir m_gameRoot; + FlameAPI api; QFileInfoList files; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp deleted file mode 100644 index 8c25b0482..000000000 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "FlamePackIndex.h" -#include -#include - -#include "Json.h" -#include "modplatform/ModIndex.h" - -void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) -{ - pack.addonId = Json::requireInteger(obj, "id"); - pack.name = Json::requireString(obj, "name"); - pack.description = Json::ensureString(obj, "summary", ""); - - auto logo = Json::requireObject(obj, "logo"); - pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); - pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix(); - - auto authors = Json::requireArray(obj, "authors"); - for (auto authorIter : authors) { - auto author = Json::requireObject(authorIter); - Flame::ModpackAuthor packAuthor; - packAuthor.name = Json::requireString(author, "name"); - packAuthor.url = Json::requireString(author, "url"); - pack.authors.append(packAuthor); - } - int defaultFileId = Json::requireInteger(obj, "mainFileId"); - - bool found = false; - // check if there are some files before adding the pack - auto files = Json::requireArray(obj, "latestFiles"); - for (auto fileIter : files) { - auto file = Json::requireObject(fileIter); - int id = Json::requireInteger(file, "id"); - - // NOTE: for now, ignore everything that's not the default... - if (id != defaultFileId) { - continue; - } - - auto versionArray = Json::requireArray(file, "gameVersions"); - if (versionArray.size() < 1) { - continue; - } - - found = true; - break; - } - if (!found) { - throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); - } - - loadIndexedInfo(pack, obj); -} - -void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) -{ - auto links_obj = Json::ensureObject(obj, "links"); - - pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl"); - if (pack.extra.websiteUrl.endsWith('/')) - pack.extra.websiteUrl.chop(1); - - pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); - if (pack.extra.issuesUrl.endsWith('/')) - pack.extra.issuesUrl.chop(1); - - pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); - if (pack.extra.sourceUrl.endsWith('/')) - pack.extra.sourceUrl.chop(1); - - pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); - if (pack.extra.wikiUrl.endsWith('/')) - pack.extra.wikiUrl.chop(1); - - pack.extraInfoLoaded = true; -} - -void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) -{ - QVector unsortedVersions; - for (auto versionIter : arr) { - auto version = Json::requireObject(versionIter); - Flame::IndexedVersion file; - - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(version, "id"); - auto versionArray = Json::requireArray(version, "gameVersions"); - if (versionArray.size() < 1) { - continue; - } - - for (auto mcVer : versionArray) { - auto str = mcVer.toString(); - - if (str.contains('.')) - file.mcVersion.append(str); - - if (auto loader = str.toLower(); loader == "neoforge") - file.loaders |= ModPlatform::NeoForge; - else if (loader == "forge") - file.loaders |= ModPlatform::Forge; - else if (loader == "cauldron") - file.loaders |= ModPlatform::Cauldron; - else if (loader == "liteloader") - file.loaders |= ModPlatform::LiteLoader; - else if (loader == "fabric") - file.loaders |= ModPlatform::Fabric; - else if (loader == "quilt") - file.loaders |= ModPlatform::Quilt; - } - - // pick the latest version supported - file.version = Json::requireString(version, "displayName"); - - ModPlatform::IndexedVersionType::VersionType ver_type; - switch (Json::requireInteger(version, "releaseType")) { - case 1: - ver_type = ModPlatform::IndexedVersionType::VersionType::Release; - break; - case 2: - ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; - break; - case 3: - ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; - break; - default: - ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; - } - file.version_type = ModPlatform::IndexedVersionType(ver_type); - file.downloadUrl = Json::ensureString(version, "downloadUrl"); - - // only add if we have a download URL (third party distribution is enabled) - if (!file.downloadUrl.isEmpty()) { - unsortedVersions.append(file); - } - } - - auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; }; - std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); - pack.versions = unsortedVersions; - pack.versionsLoaded = true; -} diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h deleted file mode 100644 index 11633deee..000000000 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "modplatform/ModIndex.h" - -namespace Flame { - -struct ModpackAuthor { - QString name; - QString url; -}; - -struct IndexedVersion { - int addonId; - int fileId; - QString version; - ModPlatform::IndexedVersionType version_type; - ModPlatform::ModLoaderTypes loaders = {}; - QString mcVersion; - QString downloadUrl; -}; - -struct ModpackExtra { - QString websiteUrl; - QString wikiUrl; - QString issuesUrl; - QString sourceUrl; -}; - -struct IndexedPack { - int addonId; - QString name; - QString description; - QList authors; - QString logoName; - QString logoUrl; - - bool versionsLoaded = false; - QVector versions; - - bool extraInfoLoaded = false; - ModpackExtra extra; -}; - -void loadIndexedPack(IndexedPack& m, QJsonObject& obj); -void loadIndexedInfo(IndexedPack&, QJsonObject&); -void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); -} // namespace Flame - -Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 278105f4a..dc176d770 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -5,13 +5,13 @@ static void loadFileV1(Flame::File& f, QJsonObject& file) { f.projectId = Json::requireInteger(file, "projectID"); f.fileId = Json::requireInteger(file, "fileID"); - f.required = Json::ensureBoolean(file, QString("required"), true); + f.required = file["required"].toBool(true); } static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) { m.id = Json::requireString(modLoader, "id"); - m.primary = Json::ensureBoolean(modLoader, QString("primary"), false); + m.primary = modLoader["primary"].toBool(); } static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) @@ -19,14 +19,15 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) m.version = Json::requireString(minecraft, "version"); // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing - m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); - auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); + m.libraries = minecraft["libraries"].toString(); + auto arr = minecraft["modLoaders"].toArray(); for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; loadModloaderV1(loader, obj); m.modLoaders.append(loader); } + m.recommendedRAM = minecraft["recommendedRam"].toInt(); } static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) @@ -35,11 +36,11 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) loadMinecraftV1(pack.minecraft, mc); - pack.name = Json::ensureString(manifest, QString("name"), "Unnamed"); - pack.version = Json::ensureString(manifest, QString("version"), QString()); - pack.author = Json::ensureString(manifest, QString("author"), "Anonymous"); + pack.name = manifest["name"].toString("Unnamed"); + pack.version = manifest["version"].toString(); + pack.author = manifest["author"].toString("Anonymous"); - auto arr = Json::ensureArray(manifest, "files", QJsonArray()); + auto arr = manifest["files"].toArray(); for (auto item : arr) { auto obj = Json::requireObject(item); @@ -49,7 +50,7 @@ static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) pack.files.insert(file.fileId, file); } - pack.overrides = Json::ensureString(manifest, "overrides", "overrides"); + pack.overrides = manifest["overrides"].toString("overrides"); pack.is_loaded = true; } diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 49a0b2d68..049a99871 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -36,11 +36,12 @@ #pragma once #include +#include #include #include #include -#include #include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" namespace Flame { struct File { @@ -54,6 +55,7 @@ struct File { // our QString targetFolder = QStringLiteral("mods"); + ModPlatform::ResourceType resourceType; }; struct Modloader { @@ -64,7 +66,8 @@ struct Modloader { struct Minecraft { QString version; QString libraries; - QVector modLoaders; + QList modLoaders; + int recommendedRAM; }; struct Manifest { diff --git a/launcher/modplatform/helpers/ExportToModList.cpp b/launcher/modplatform/helpers/ExportToModList.cpp index aea16ab50..2432853c9 100644 --- a/launcher/modplatform/helpers/ExportToModList.cpp +++ b/launcher/modplatform/helpers/ExportToModList.cpp @@ -28,7 +28,7 @@ QString toHTML(QList mods, OptionalData extraData) auto meta = mod->metadata(); auto modName = mod->name().toHtmlEscaped(); if (extraData & Url) { - auto url = mod->metaurl().toHtmlEscaped(); + auto url = mod->homepage().toHtmlEscaped(); if (!url.isEmpty()) modName = QString("%2").arg(url, modName); } @@ -65,7 +65,7 @@ QString toMarkdown(QList mods, OptionalData extraData) auto meta = mod->metadata(); auto modName = toMarkdownEscaped(mod->name()); if (extraData & Url) { - auto url = mod->metaurl(); + auto url = mod->homepage(); if (!url.isEmpty()) modName = QString("[%1](%2)").arg(modName, url); } @@ -95,7 +95,7 @@ QString toPlainTXT(QList mods, OptionalData extraData) auto line = modName; if (extraData & Url) { - auto url = mod->metaurl(); + auto url = mod->homepage(); if (!url.isEmpty()) line += QString(" (%1)").arg(url); } @@ -124,7 +124,7 @@ QString toJSON(QList mods, OptionalData extraData) QJsonObject line; line["name"] = modName; if (extraData & Url) { - auto url = mod->metaurl(); + auto url = mod->homepage(); if (!url.isEmpty()) line["url"] = url; } @@ -156,7 +156,7 @@ QString toCSV(QList mods, OptionalData extraData) data << modName; if (extraData & Url) - data << mod->metaurl(); + data << mod->homepage(); if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) @@ -203,7 +203,8 @@ QString exportToModList(QList mods, QString lineTemplate) for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); - auto url = mod->metaurl(); + auto modID = mod->mod_id(); + auto url = mod->homepage(); auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); @@ -211,6 +212,7 @@ QString exportToModList(QList mods, QString lineTemplate) auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) + .replace("{mod_id}", modID) .replace("{url}", url) .replace("{version}", ver) .replace("{authors}", authors) @@ -218,4 +220,4 @@ QString exportToModList(QList mods, QString lineTemplate) } return lines.join("\n"); } -} // namespace ExportToModList \ No newline at end of file +} // namespace ExportToModList diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp deleted file mode 100644 index 7a234a8f8..000000000 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only - -#include "NetworkResourceAPI.h" -#include - -#include "Application.h" -#include "net/NetJob.h" - -#include "modplatform/ModIndex.h" - -#include "net/ApiDownload.h" - -Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const -{ - auto search_url_optional = getSearchURL(args); - if (!search_url_optional.has_value()) { - callbacks.on_fail("Failed to create search URL", -1); - return nullptr; - } - - auto search_url = search_url_optional.value(); - - auto response = std::make_shared(); - auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response)); - - QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - callbacks.on_fail(parse_error.errorString(), -1); - - return; - } - - callbacks.on_succeed(doc); - }); - - // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. - // This prevents the lambda from extending the lifetime of the shared resource, - // as it only temporarily locks the resource when needed. - auto weak = netJob.toWeakRef(); - QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto netJob = weak.lock()) { - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); - } - callbacks.on_fail(reason, network_error_code); - }); - QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); - - return netJob; -} - -Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const -{ - auto response = std::make_shared(); - auto job = getProject(args.pack.addonId.toString(), response); - - QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] { - 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(); - qWarning() << *response; - return; - } - - callbacks.on_succeed(doc, args.pack); - }); - QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); - QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); - return job; -} - -Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const -{ - auto versions_url_optional = getVersionsURL(args); - if (!versions_url_optional.has_value()) - return nullptr; - - auto versions_url = versions_url_optional.value(); - - auto netJob = makeShared(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); - auto response = std::make_shared(); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - - QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - callbacks.on_succeed(doc, args.pack); - }); - - // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. - // This prevents the lambda from extending the lifetime of the shared resource, - // as it only temporarily locks the resource when needed. - auto weak = netJob.toWeakRef(); - QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto netJob = weak.lock()) { - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); - } - callbacks.on_fail(reason, network_error_code); - }); - - return netJob; -} - -Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr response) const -{ - auto project_url_optional = getInfoURL(addonId); - if (!project_url_optional.has_value()) - return nullptr; - - auto project_url = project_url_optional.value(); - - auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); - - return netJob; -} - -Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const -{ - auto versions_url_optional = getDependencyURL(args); - if (!versions_url_optional.has_value()) - return nullptr; - - 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)); - - QObject::connect(netJob.get(), &NetJob::succeeded, [=] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - callbacks.on_succeed(doc, args.dependency); - }); - - // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. - // This prevents the lambda from extending the lifetime of the shared resource, - // as it only temporarily locks the resource when needed. - auto weak = netJob.toWeakRef(); - QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { - int network_error_code = -1; - if (auto netJob = weak.lock()) { - if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) - network_error_code = failed_action->replyStatusCode(); - } - callbacks.on_fail(reason, network_error_code); - }); - return netJob; -} diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h deleted file mode 100644 index b72e82533..000000000 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include "modplatform/ResourceAPI.h" - -class NetworkResourceAPI : public ResourceAPI { - public: - Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; - - Task::Ptr getProject(QString addonId, std::shared_ptr response) const override; - - Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; - Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; - Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override; - - protected: - [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; - [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional = 0; - [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; - [[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; -}; diff --git a/launcher/modplatform/helpers/OverrideUtils.cpp b/launcher/modplatform/helpers/OverrideUtils.cpp index 60983a5cf..d5958a59d 100644 --- a/launcher/modplatform/helpers/OverrideUtils.cpp +++ b/launcher/modplatform/helpers/OverrideUtils.cpp @@ -15,7 +15,10 @@ void createOverrides(const QString& name, const QString& parent_folder, const QS FS::ensureFilePathExists(file_path); QFile file(file_path); - file.open(QFile::WriteOnly); + if (!file.open(QFile::WriteOnly)) { + qWarning() << "Failed to open file '" << file.fileName() << "' for writing!"; + return; + } QDirIterator override_iterator(override_path, QDirIterator::Subdirectories); while (override_iterator.hasNext()) { @@ -43,7 +46,10 @@ QStringList readOverrides(const QString& name, const QString& parent_folder) QStringList previous_overrides; - file.open(QFile::ReadOnly); + if (!file.open(QFile::ReadOnly)) { + qWarning() << "Failed to open file '" << file.fileName() << "' for reading!"; + return previous_overrides; + } QString entry; do { diff --git a/launcher/modplatform/import_ftb/PackHelpers.cpp b/launcher/modplatform/import_ftb/PackHelpers.cpp index e523b9d20..cf9edce9f 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.cpp +++ b/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -19,6 +19,7 @@ #include "modplatform/import_ftb/PackHelpers.h" #include +#include #include #include @@ -27,6 +28,35 @@ namespace FTBImportAPP { +QIcon loadFTBIcon(const QString& imagePath) +{ + // Map of type byte to image type string + static const QHash imageTypeMap = { { 0x00, "png" }, { 0x01, "jpg" }, { 0x02, "gif" }, { 0x03, "webp" } }; + QFile file(imagePath); + if (!file.exists() || !file.open(QIODevice::ReadOnly)) { + return QIcon(); + } + char type; + if (!file.getChar(&type)) { + qDebug() << "Missing FTB image type header at" << imagePath; + return QIcon(); + } + if (!imageTypeMap.contains(type)) { + qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16); + return QIcon(); + } + + auto imageType = imageTypeMap[type]; + // Extract actual image data beyond the first byte + QImageReader reader(&file, imageType); + auto pixmap = QPixmap::fromImageReader(&reader); + if (pixmap.isNull()) { + qDebug() << "The FTB image at" << imagePath << "is not valid"; + return QIcon(); + } + return QIcon(pixmap); +} + Modpack parseDirectory(QString path) { Modpack modpack{ path }; @@ -42,15 +72,53 @@ Modpack parseDirectory(QString path) modpack.name = Json::requireString(root, "name", "name"); modpack.version = Json::requireString(root, "version", "version"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); - modpack.jvmArgs = Json::ensureVariant(root, "jvmArgs", {}, "jvmArgs"); + modpack.jvmArgs = root["jvmArgs"].toVariant(); modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); + + auto modLoader = Json::requireString(root, "modLoader", "modLoader"); + if (!modLoader.isEmpty()) { + const auto parts = modLoader.split('-', Qt::KeepEmptyParts); + if (parts.size() >= 2) { + const auto loader = parts.first().toLower(); + modpack.loaderVersion = parts.at(1).trimmed(); + if (loader == "neoforge") { + modpack.loaderType = ModPlatform::NeoForge; + } else if (loader == "forge") { + modpack.loaderType = ModPlatform::Forge; + } else if (loader == "fabric") { + modpack.loaderType = ModPlatform::Fabric; + } else if (loader == "quilt") { + modpack.loaderType = ModPlatform::Quilt; + } + } + } } catch (const Exception& e) { qDebug() << "Couldn't load ftb instance json: " << e.cause(); return {}; } - auto versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); - if (!versionsFile.exists() || !versionsFile.isFile()) - return {}; + if (!modpack.loaderType.has_value()) { + legacyInstanceParsing(path, &modpack.loaderType, &modpack.loaderVersion); + } + + auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); + if (iconFile.exists() && iconFile.isFile()) { + modpack.icon = QIcon(iconFile.absoluteFilePath()); + } else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data + modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo")); + } + return modpack; +} + +void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion) +{ + auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json")); + if (!versionsFile.exists() || !versionsFile.isFile()) { + versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); + } + if (!versionsFile.exists() || !versionsFile.isFile()) { + qDebug() << "Couldn't find ftb version json"; + return; + } try { auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file"); const auto root = doc.object(); @@ -61,32 +129,26 @@ Modpack parseDirectory(QString path) auto name = Json::requireString(obj, "name", "name"); auto version = Json::requireString(obj, "version", "version"); if (name == "neoforge") { - modpack.loaderType = ModPlatform::NeoForge; - modpack.version = version; + *loaderType = ModPlatform::NeoForge; + *loaderVersion = version; break; } else if (name == "forge") { - modpack.loaderType = ModPlatform::Forge; - modpack.version = version; + *loaderType = ModPlatform::Forge; + *loaderVersion = version; break; } else if (name == "fabric") { - modpack.loaderType = ModPlatform::Fabric; - modpack.version = version; + *loaderType = ModPlatform::Fabric; + *loaderVersion = version; break; } else if (name == "quilt") { - modpack.loaderType = ModPlatform::Quilt; - modpack.version = version; + *loaderType = ModPlatform::Quilt; + *loaderVersion = version; break; } } } catch (const Exception& e) { qDebug() << "Couldn't load ftb version json: " << e.cause(); - return {}; + return; } - auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); - if (iconFile.exists() && iconFile.isFile()) { - modpack.icon = QIcon(iconFile.absoluteFilePath()); - } - return modpack; } - } // namespace FTBImportAPP diff --git a/launcher/modplatform/import_ftb/PackHelpers.h b/launcher/modplatform/import_ftb/PackHelpers.h index 449ed2546..f010313ff 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.h +++ b/launcher/modplatform/import_ftb/PackHelpers.h @@ -49,7 +49,7 @@ struct Modpack { using ModpackList = QList; Modpack parseDirectory(QString path); - +void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion); } // namespace FTBImportAPP // We need it for the proxy model diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 8046300e1..c5f80990e 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -50,10 +50,11 @@ 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); + MinecraftInstance instance(m_globalSettings, std::make_unique(instanceConfigPath), m_stagingPath); + SettingsObject::Lock lock(instance.settings()); + instance.settings()->set("InstanceType", "OneSix"); instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); @@ -70,25 +71,37 @@ void PackInstallTask::copySettings() if (modloader.has_value()) switch (modloader.value()) { case ModPlatform::NeoForge: { - components->setComponentVersion("net.neoforged", m_pack.version, true); + components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); break; } case ModPlatform::Forge: { - components->setComponentVersion("net.minecraftforge", m_pack.version, true); + components->setComponentVersion("net.minecraftforge", m_pack.loaderVersion, true); break; } case ModPlatform::Fabric: { - components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.version, true); + components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.loaderVersion, true); break; } case ModPlatform::Quilt: { - components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.version, true); + 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; } components->saveNow(); @@ -96,7 +109,6 @@ void PackInstallTask::copySettings() if (m_instIcon == "default") m_instIcon = "ftb_logo"; instance.setIconKey(m_instIcon); - instanceSettings->resumeSave(); emitSucceeded(); } diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index a0beeddcc..43a5fbfa2 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -53,15 +53,15 @@ 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)); + jobPtr->addNetAction(Net::ApiDownload::makeByteArray(publicPacksUrl, publicModpacksXmlFileData.get())); 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)); + jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData.get())); - QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); - QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); - QObject::connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); + connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); + connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); jobPtr->start(); } @@ -73,13 +73,13 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) 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)); + job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data.get())); job->setAskRetry(false); - QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { + connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); - foreach (Modpack currentPack, packs) { + for (auto& currentPack : packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } @@ -89,14 +89,14 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) data->clear(); }); - QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { + connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); data->clear(); }); - QObject::connect(job, &NetJob::aborted, this, [this, job, data] { + connect(job, &NetJob::aborted, this, [this, job, data] { emit aborted(); job->deleteLater(); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h index e37d949d5..dd119c068 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -13,18 +13,18 @@ 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(); + std::unique_ptr publicModpacksXmlFileData = std::make_unique(); + std::unique_ptr thirdPartyModpacksXmlFileData = std::make_unique(); bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; @@ -40,7 +40,7 @@ class PackFetchTask : public QObject { void failed(QString reason); void aborted(); - void privateFileDownloadFinished(Modpack modpack); + void privateFileDownloadFinished(const Modpack& modpack); void privateFileDownloadFailed(QString reason, QString packCode); }; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index d6252663f..51e4f4b9d 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -52,7 +52,7 @@ namespace LegacyFTB { -PackInstallTask::PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version) +PackInstallTask::PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version) { m_pack = pack; m_version = version; @@ -102,19 +102,8 @@ void PackInstallTask::unzip() QDir extractDir(m_stagingPath); - m_packZip.reset(new QuaZip(archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) { - emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); - return; - } - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); -#else - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); -#endif connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); @@ -144,10 +133,9 @@ 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); @@ -158,25 +146,29 @@ void PackInstallTask::install() QFile packJson(m_stagingPath + "/minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); if (packJson.exists()) { - 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(); - - foreach (const QJsonValue& value, libs) { - QString nameValue = value.toObject().value("name").toString(); - if (!nameValue.startsWith("net.minecraftforge")) { - continue; + 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(); + + 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!"; } } @@ -211,7 +203,6 @@ void PackInstallTask::install() 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 30ff48597..98777214f 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -1,6 +1,4 @@ #pragma once -#include -#include #include "InstanceTask.h" #include "PackHelpers.h" #include "meta/Index.h" @@ -8,8 +6,6 @@ #include "meta/VersionList.h" #include "net/NetJob.h" -#include "net/NetJob.h" - #include namespace LegacyFTB { @@ -18,7 +14,7 @@ class PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version); + explicit PackInstallTask(QNetworkAccessManager* network, const Modpack& pack, QString version); virtual ~PackInstallTask() {} bool canAbort() const override { return true; } @@ -39,9 +35,8 @@ class PackInstallTask : public InstanceTask { void onUnzipCanceled(); private: /* data */ - shared_qobject_ptr m_network; + QNetworkAccessManager* m_network; bool abortable = false; - std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; NetJob::Ptr netJobContainer; diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp index 2ae351329..17e9f7d76 100644 --- a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp +++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -44,12 +44,8 @@ namespace LegacyFTB { void PrivatePackManager::load() { try { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); currentPacks = QSet(foo.begin(), foo.end()); -#else - currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet(); -#endif dirty = false; } catch (...) { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index bdef1a0e5..ecbba4c75 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -11,7 +11,7 @@ #include "net/NetJob.h" #include "net/Upload.h" -Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) +Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); @@ -21,7 +21,7 @@ Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::sh return netJob; } -Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) +Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); @@ -42,7 +42,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, - std::shared_ptr response) + QByteArray* response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -72,7 +72,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, - std::shared_ptr response) + QByteArray* response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -101,7 +101,7 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, return netJob; } -Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const +Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); @@ -121,7 +121,7 @@ QList ModrinthAPI::getSortingMethods() const { 5, "updated", QObject::tr("Sort by Last Updated") } }; } -Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) +Task::Ptr ModrinthAPI::getModCategories(QByteArray* response) { auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response)); @@ -129,7 +129,7 @@ Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) return netJob; } -QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) +QList ModrinthAPI::loadCategories(QByteArray* response, QString projectType) { QList categories; QJsonParseError parse_error{}; @@ -147,7 +147,7 @@ QList ModrinthAPI::loadCategories(std::shared_ptr ModrinthAPI::loadCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr response) +QList ModrinthAPI::loadModCategories(QByteArray* response) { return loadCategories(response, "mod"); }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 4c82a5ec6..acfe396b7 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -5,45 +5,48 @@ #pragma once #include "BuildConfig.h" +#include "Json.h" #include "modplatform/ModIndex.h" -#include "modplatform/helpers/NetworkResourceAPI.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" #include -class ModrinthAPI : public NetworkResourceAPI { +class ModrinthAPI : public ResourceAPI { public: - auto currentVersion(QString hash, QString hash_format, std::shared_ptr response) -> Task::Ptr; + Task::Ptr currentVersion(QString hash, QString hash_format, QByteArray* response); - auto currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) -> Task::Ptr; + Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response); - auto latestVersion(QString hash, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) -> Task::Ptr; + Task::Ptr latestVersion(QString hash, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + QByteArray* response); - auto latestVersions(const QStringList& hashes, - QString hash_format, - std::optional> mcVersions, - std::optional loaders, - std::shared_ptr response) -> Task::Ptr; + Task::Ptr latestVersions(const QStringList& hashes, + QString hash_format, + std::optional> mcVersions, + std::optional loaders, + QByteArray* response); - Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; + Task::Ptr getProjects(QStringList addonIds, QByteArray* response) const override; - static Task::Ptr getModCategories(std::shared_ptr response); - static QList loadCategories(std::shared_ptr response, QString projectType); - static QList loadModCategories(std::shared_ptr response); + static Task::Ptr getModCategories(QByteArray* response); + static QList loadCategories(QByteArray* response, QString projectType); + static QList loadModCategories(QByteArray* response); public: - [[nodiscard]] auto getSortingMethods() const -> QList override; + auto getSortingMethods() const -> QList override; inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : - { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { + 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) { l << getModLoaderAsString(loader); } @@ -69,19 +72,23 @@ class ModrinthAPI : public NetworkResourceAPI { return l.join(','); } - static auto getSideFilters(QString side) -> const QString + static QString getSideFilters(ModPlatform::Side side) { - if (side.isEmpty() || side == "both") { - return {}; + switch (side) { + case ModPlatform::Side::ClientSide: + return QString("\"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\""); + case ModPlatform::Side::UniversalSide: + return QString("\"client_side:required\"],[\"server_side:required\""); + case ModPlatform::Side::NoSide: + // fallthrough + default: + return {}; } - if (side == "client") - return QString("\"client_side:required\",\"client_side:optional\""); - if (side == "server") - return QString("\"server_side:required\",\"server_side:optional\""); - return {}; } - [[nodiscard]] static inline QString mapMCVersionFromModrinth(QString v) + static inline QString mapMCVersionFromModrinth(QString v) { static const QString preString = " Pre-Release "; bool pre = false; @@ -97,16 +104,18 @@ class ModrinthAPI : public NetworkResourceAPI { } private: - [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) + static QString resourceTypeParameter(ModPlatform::ResourceType type) { switch (type) { - case ModPlatform::ResourceType::MOD: + case ModPlatform::ResourceType::Mod: return "mod"; - case ModPlatform::ResourceType::RESOURCE_PACK: + case ModPlatform::ResourceType::ResourcePack: return "resourcepack"; - case ModPlatform::ResourceType::SHADER_PACK: + case ModPlatform::ResourceType::ShaderPack: return "shader"; - case ModPlatform::ResourceType::MODPACK: + case ModPlatform::ResourceType::DataPack: + return "datapack"; + case ModPlatform::ResourceType::Modpack: return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; @@ -116,7 +125,7 @@ class ModrinthAPI : public NetworkResourceAPI { return ""; } - [[nodiscard]] QString createFacets(SearchArgs const& args) const + QString createFacets(SearchArgs const& args) const { QStringList facets_list; @@ -131,6 +140,8 @@ class ModrinthAPI : public NetworkResourceAPI { } if (args.categoryIds.has_value() && !args.categoryIds->empty()) facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); + if (args.openSource) + facets_list.append("[\"open_source:true\"]"); facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); @@ -138,7 +149,7 @@ class ModrinthAPI : public NetworkResourceAPI { } public: - [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override + inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { @@ -178,7 +189,7 @@ class ModrinthAPI : public NetworkResourceAPI { get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); return QString("%1/project/%2/version%3%4") - .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); + .arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; QString getGameVersionsArray(std::list mcVersions) const @@ -193,10 +204,12 @@ class ModrinthAPI : public NetworkResourceAPI { static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { - return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader); + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | + ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | + ModPlatform::Ornithe | ModPlatform::Rift); } - [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + std::optional getDependencyURL(DependencySearchArgs const& 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\"]") @@ -205,4 +218,12 @@ class ModrinthAPI : public NetworkResourceAPI { .arg(mapMCVersionToModrinth(args.mcVersion)) .arg(getModLoaderStrings(args.loader).join("\",\"")); }; + + 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 + { + return Modrinth::loadIndexedPackVersion(obj); + }; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); } }; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index fcabe24fc..b24da6fc9 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -15,13 +15,25 @@ static ModrinthAPI api; -ModrinthCheckUpdate::ModrinthCheckUpdate(QList& mods, +ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder) - : CheckUpdateTask(mods, mcVersions, loadersList, mods_folder) - , m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) -{} + 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) { + modLoaders |= m->metadata()->loaders; + } + for (auto l : m_loadersList) { + modLoaders &= ~l; + } + m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); + } +} bool ModrinthCheckUpdate::abort() { @@ -37,36 +49,70 @@ bool ModrinthCheckUpdate::abort() * */ void ModrinthCheckUpdate::executeTask() { - setStatus(tr("Preparing mods for Modrinth...")); - setProgress(0, 9); + setStatus(tr("Preparing resources for Modrinth...")); + setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1); auto hashing_task = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); - for (auto* mod : m_mods) { - auto hash = mod->metadata()->hash; + bool startHasing = false; + for (auto* resource : m_resources) { + auto hash = resource->metadata()->hash; // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) - if (mod->metadata()->hash_format != m_hash_type) { - auto hash_task = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); - connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { m_mappings.insert(hash, mod); }); + if (resource->metadata()->hash_format != m_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); + startHasing = true; } else { - m_mappings.insert(hash, mod); + m_mappings.insert(hash, resource); } } - connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); - m_job = hashing_task; - hashing_task->start(); + if (startHasing) { + connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); + m_job = hashing_task; + hashing_task->start(); + } else { + checkNextLoader(); + } } -void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, - ModPlatform::ModLoaderTypes loader, - bool forceModLoaderCheck) +void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader, bool forceModLoaderCheck) { + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(m_progress + 1, m_progressTotal); + + auto response = std::make_shared(); + QStringList hashes; + if (forceModLoaderCheck && loader.has_value()) { + for (auto hash : m_mappings.keys()) { + if (m_mappings[hash]->metadata()->loaders & loader.value()) { + hashes.append(hash); + } + } + } else { + hashes = m_mappings.keys(); + } + auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response.get()); + + connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response.get(), loader); }); + + connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); + + m_job = job; + m_loaderIdx++; + job->start(); +} + +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) { @@ -78,29 +124,31 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp return; } - setStatus(tr("Parsing the API response from Modrinth...")); - setProgress(m_next_loader_idx * 2, 9); - try { - for (auto hash : m_mappings.keys()) { - if (forceModLoaderCheck && !(m_mappings[hash]->loaders() & loader)) { - continue; - } + auto iter = m_mappings.begin(); + + while (iter != m_mappings.end()) { + const QString hash = iter.key(); + Resource* resource = iter.value(); + auto project_obj = doc[hash].toObject(); // If the returned project is empty, but we have Modrinth metadata, // it means this specific version is not available if (project_obj.isEmpty()) { qDebug() << "Mod " << m_mappings.find(hash).value()->name() << " got an empty response." << "Hash: " << hash; + ++iter; continue; } // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it QString loader_filter; - for (auto flag : ModPlatform::modLoaderTypesToList(loader)) { - loader_filter = ModPlatform::getModLoaderAsString(flag); - break; + if (loader.has_value()) { + for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) { + loader_filter = ModPlatform::getModLoaderAsString(flag); + break; + } } // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: @@ -109,109 +157,70 @@ 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_hash_type, loader_filter); + 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; - + ++iter; continue; } - auto mod_iter = m_mappings.find(hash); - if (mod_iter == m_mappings.end()) { - qCritical() << "Failed to remap mod from Modrinth!"; - continue; - } - auto mod = *mod_iter; - m_mappings.remove(hash); - - auto key = project_ver.hash; - // Fake pack with the necessary info to pass to the download task :) auto pack = std::make_shared(); - pack->name = mod->name(); - pack->slug = mod->metadata()->slug; - pack->addonId = mod->metadata()->project_id; - pack->websiteUrl = mod->homeurl(); - for (auto& author : mod->authors()) - pack->authors.append({ author }); - pack->description = mod->description(); + pack->name = resource->name(); + pack->slug = resource->metadata()->slug; + pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; - if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) { - if (mod->version() == project_ver.version_number) - continue; - - auto download_task = makeShared(pack, project_ver, m_mods_folder); + if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { + auto download_task = makeShared(pack, project_ver, m_resourceModel); + + QString old_version = resource->metadata()->version_number; + if (old_version.isEmpty()) { + if (resource->status() == ResourceStatus::NOT_INSTALLED) + old_version = tr("Not installed"); + else + old_version = tr("Unknown"); + } - m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type, - project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, mod->enabled()); + m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, + project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, resource->enabled()); } m_deps.append(std::make_shared(pack, project_ver)); + + iter = m_mappings.erase(iter); } } catch (Json::JsonException& e) { - emitFailed(e.cause() + " : " + e.what()); + emitFailed(e.cause() + ": " + e.what()); return; } checkNextLoader(); } -void ModrinthCheckUpdate::getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck) -{ - auto response = std::make_shared(); - QStringList hashes; - if (forceModLoaderCheck) { - for (auto hash : m_mappings.keys()) { - if (m_mappings[hash]->loaders() & loader) { - hashes.append(hash); - } - } - } else { - hashes = m_mappings.keys(); - } - auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); - - connect(job.get(), &Task::succeeded, this, - [this, response, loader, forceModLoaderCheck] { checkVersionsResponse(response, loader, forceModLoaderCheck); }); - - connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); - - setStatus(tr("Waiting for the API response from Modrinth...")); - setProgress(m_next_loader_idx * 2 - 1, 9); - - m_job = job; - job->start(); -} - void ModrinthCheckUpdate::checkNextLoader() { if (m_mappings.isEmpty()) { emitSucceeded(); return; } - if (m_next_loader_idx < m_loaders_list.size()) { - getUpdateModsForLoader(m_loaders_list.at(m_next_loader_idx)); - m_next_loader_idx++; + if (m_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 + getUpdateModsForLoader(); return; } - static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, ModPlatform::ModLoaderType::Quilt, - ModPlatform::ModLoaderType::Fabric }; - for (auto flag : flags) { - if (!m_loaders_list.contains(flag)) { - m_loaders_list.append(flag); - m_next_loader_idx++; - setProgress(m_next_loader_idx * 2 - 1, 9); - for (auto m : m_mappings) { - if (m->loaders() & flag) { - getUpdateModsForLoader(flag, true); - return; - } - } - setProgress(m_next_loader_idx * 2, 9); - } - } - for (auto m : m_mappings) { - emit checkFailed(m, - tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + + for (auto resource : m_mappings) { + QString reason; + + if (dynamic_cast(resource) != nullptr) + reason = + tr("No valid version found for this resource. It's probably unavailable for the current game " + "version / mod loader."); + else + reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); + + emit checkFailed(resource, reason); } + emitSucceeded(); - return; } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index dab4bda2f..e44c782d1 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -6,23 +6,24 @@ class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - ModrinthCheckUpdate(QList& mods, + ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr mods_folder); + ResourceFolderModel* resourceModel); public slots: bool abort() override; protected slots: void executeTask() override; - void getUpdateModsForLoader(ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); - void checkVersionsResponse(std::shared_ptr response, ModPlatform::ModLoaderTypes loader, bool forceModLoaderCheck = false); + void getUpdateModsForLoader(std::optional loader = {}, bool forceModLoaderCheck = false); + void checkVersionsResponse(QByteArray* response, std::optional loader); void checkNextLoader(); private: Task::Ptr m_job = nullptr; - QHash m_mappings; - QString m_hash_type; - int m_next_loader_idx = 0; + QHash m_mappings; + QString m_hashType; + int m_loaderIdx = 0; + int m_initialSize = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 3592d2ed7..45ac61273 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -13,7 +13,6 @@ #include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" @@ -44,7 +43,7 @@ bool ModrinthCreationTask::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); @@ -85,7 +84,7 @@ bool ModrinthCreationTask::updateInstance() 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; + std::vector old_files; parseManifest(old_index_path, old_files, false, false); // Let's remove all duplicated, identical resources! @@ -121,6 +120,11 @@ bool ModrinthCreationTask::updateInstance() continue; qDebug() << "Scheduling" << file.path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); + if (file.path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9))); + } else { + m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled")); + } } } @@ -208,8 +212,8 @@ bool ModrinthCreationTask::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); + MinecraftInstance instance(m_globalSettings, std::move(instanceSettings), m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); @@ -243,8 +247,9 @@ bool ModrinthCreationTask::createInstance() auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); - QHash mods; - for (auto file : m_files) { + // 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); @@ -259,7 +264,7 @@ bool ModrinthCreationTask::createInstance() ModDetails d; d.mod_id = file_path; mod->setDetails(d); - mods[file.hash.toHex()] = mod; + resources[file.hash.toHex()] = mod; } if (file.downloads.empty()) { setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); @@ -285,13 +290,13 @@ bool ModrinthCreationTask::createInstance() bool ended_well = false; - connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); - connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) { + connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); + connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](const QString& reason) { ended_well = false; setError(reason); }); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); - connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(downloadMods.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); @@ -304,18 +309,18 @@ bool ModrinthCreationTask::createInstance() loop.exec(); if (!ended_well) { - for (auto m : mods) { - delete m; + for (auto resource : resources) { + delete resource; } return ended_well; } QEventLoop ensureMetaLoop; QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); - auto ensureMetadataTask = makeShared(mods, folder, ModPlatform::ResourceProvider::MODRINTH); - connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; }); + auto ensureMetadataTask = makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); + connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; }); connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); - connect(ensureMetadataTask.get(), &Task::progress, [&](qint64 current, qint64 total) { + connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); @@ -325,10 +330,10 @@ bool ModrinthCreationTask::createInstance() m_task = ensureMetadataTask; ensureMetaLoop.exec(); - for (auto m : mods) { - delete m; + for (auto resource : resources) { + delete resource; } - mods.clear(); + resources.clear(); // Update information of the already installed instance, if any. if (m_instance && ended_well) { @@ -350,7 +355,7 @@ bool ModrinthCreationTask::createInstance() } bool ModrinthCreationTask::parseManifest(const QString& index_path, - std::vector& files, + std::vector& files, bool set_internal_data, bool show_optional_dialog) { @@ -366,20 +371,20 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, if (set_internal_data) { if (m_managed_version_id.isEmpty()) - m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); - m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); + m_managed_version_id = obj["versionId"].toString(); + m_managed_name = obj["name"].toString(); } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - std::vector optionalFiles; + std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { - Modrinth::File file; + File file; file.path = Json::requireString(modInfo, "path").replace("\\", "/"); - auto env = Json::ensureObject(modInfo, "env"); + auto env = modInfo["env"].toObject(); // 'env' field is optional if (!env.isEmpty()) { - QString support = Json::ensureString(env, "client", "unsupported"); + QString support = env["client"].toString("unsupported"); if (support == "unsupported") { continue; } else if (support == "optional") { @@ -394,7 +399,7 @@ 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 = Json::ensureArray(modInfo, "downloads"); + auto download_arr = modInfo["downloads"].toArray(); for (auto download : download_arr) { qWarning() << download.toString(); bool is_last = download.toString() == download_arr.last().toString(); @@ -415,23 +420,30 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } if (!optionalFiles.empty()) { - QStringList oFiles; - for (auto file : optionalFiles) - oFiles.push_back(file.path); - OptionalModDialog optionalModDialog(m_parent, oFiles); - if (optionalModDialog.exec() == QDialog::Rejected) { - emitAborted(); - return false; - } + if (show_optional_dialog) { + QStringList oFiles; + for (auto file : optionalFiles) + oFiles.push_back(file.path); + OptionalModDialog optionalModDialog(m_parent, oFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + return false; + } - auto selectedMods = optionalModDialog.getResult(); - for (auto file : optionalFiles) { - if (selectedMods.contains(file.path)) { - file.required = true; - } else { + auto selectedMods = optionalModDialog.getResult(); + for (auto file : optionalFiles) { + if (selectedMods.contains(file.path)) { + file.required = true; + } else { + file.path += ".disabled"; + } + files.push_back(file); + } + } else { + for (auto file : optionalFiles) { file.path += ".disabled"; + files.push_back(file); } - files.push_back(file); } } if (set_internal_data) { diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index ddfa7ae95..bda91edf6 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -1,17 +1,31 @@ #pragma once #include + +#include +#include +#include +#include +#include +#include + #include "BaseInstance.h" #include "InstanceCreationTask.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" - class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT + struct File { + QString path; + + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + QQueue downloads; + bool required = true; + }; public: ModrinthCreationTask(QString staging_path, - SettingsObjectPtr global_settings, + SettingsObject* global_settings, QWidget* parent, QString id, QString version_id = {}, @@ -30,7 +44,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { bool 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 set_internal_data = true, bool show_optional_dialog = true); private: QWidget* m_parent = nullptr; @@ -38,10 +52,10 @@ class ModrinthCreationTask final : public InstanceCreationTask { QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; QString m_managed_id, m_managed_version_id, m_managed_name; - std::vector m_files; + 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 b7c2757e5..d9002274c 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -25,9 +25,11 @@ #include #include "Json.h" #include "MMCZip.h" +#include "archive/ExportToZipTask.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" @@ -38,15 +40,15 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, - InstancePtr instance, + BaseInstance* instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFileFunction filter) : name(name) , version(version) , summary(summary) , optionalFiles(optionalFiles) , instance(instance) - , mcInstance(dynamic_cast(instance.get())) + , mcInstance(dynamic_cast(instance)) , gameRoot(instance->gameRoot()) , output(output) , filter(filter) @@ -63,7 +65,6 @@ bool ModrinthPackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -85,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(); } @@ -123,7 +124,7 @@ void ModrinthPackExportTask::collectHashes() modIter != allMods.end()) { const Mod* mod = *modIter; if (mod->metadata() != nullptr) { - QUrl& url = mod->metadata()->url; + const QUrl& url = mod->metadata()->url; // ensure the url is permitted on modrinth.com if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) { qDebug() << "Resolving" << relative << "from index"; @@ -155,14 +156,15 @@ void ModrinthPackExportTask::makeApiRequest() else { setStatus(tr("Finding versions for hashes...")); auto response = std::make_shared(); - task = api.currentVersions(pendingHashes.values(), "sha512", response); - connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); + task = api.currentVersions(pendingHashes.values(), "sha512", response.get()); + connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response.get()); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); task->start(); } } -void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) +void ModrinthPackExportTask::parseApiResponse(QByteArray* response) { task = nullptr; @@ -199,7 +201,7 @@ void ModrinthPackExportTask::buildZip() { setStatus(tr("Adding files...")); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, true); + auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->setExcludeFiles(resolvedFiles.keys()); @@ -289,7 +291,7 @@ QByteArray ModrinthPackExportTask::generateIndex() // a server side mod does not imply that the mod does not work on the client // however, if a mrpack mod is marked as server-only it will not install on the client - if (iterator->side == Metadata::ModSide::ClientSide) + if (iterator->side == ModPlatform::Side::ClientSide) env["server"] = "unsupported"; fileOut["env"] = env; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index ee740a456..5aca657a9 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -23,6 +23,7 @@ #include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/Task.h" @@ -33,9 +34,9 @@ class ModrinthPackExportTask : public Task { const QString& version, const QString& summary, bool optionalFiles, - InstancePtr instance, + BaseInstance* instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFileFunction filter); protected: void executeTask() override; @@ -45,7 +46,7 @@ class ModrinthPackExportTask : public Task { struct ResolvedFile { QString sha1, sha512, url; qint64 size; - Metadata::ModSide side; + ModPlatform::Side side; }; static const QStringList PREFIXES; @@ -54,11 +55,11 @@ 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; - const MMCZip::FilterFunction filter; + const MMCZip::FilterFileFunction filter; ModrinthAPI api; QFileInfoList files; @@ -69,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 72550937c..699e12b40 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -33,41 +33,43 @@ bool shouldDownloadOnSide(QString side) return side == "required" || side == "optional"; } -// https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject +// https://docs.modrinth.com/api/operations/getproject/ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.addonId = Json::ensureString(obj, "project_id"); + pack.addonId = obj["project_id"].toString(); if (pack.addonId.toString().isEmpty()) pack.addonId = Json::requireString(obj, "id"); pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.name = Json::requireString(obj, "title"); - pack.slug = Json::ensureString(obj, "slug", ""); + pack.slug = obj["slug"].toString(""); if (!pack.slug.isEmpty()) pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; else pack.websiteUrl = ""; - pack.description = Json::ensureString(obj, "description", ""); + pack.description = obj["description"].toString(""); - pack.logoUrl = Json::ensureString(obj, "icon_url", ""); - pack.logoName = pack.addonId.toString(); + pack.logoUrl = obj["icon_url"].toString(""); + pack.logoName = QString("%1.%2").arg(obj["slug"].toString(), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix()); - ModPlatform::ModpackAuthor modAuthor; - modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)")); - modAuthor.url = api.getAuthorURL(modAuthor.name); - pack.authors.append(modAuthor); + if (obj.contains("author")) { + ModPlatform::ModpackAuthor modAuthor; + modAuthor.name = obj["author"].toString(); + modAuthor.url = api.getAuthorURL(modAuthor.name); + pack.authors = { modAuthor }; + } - auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side")); - auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); + auto client = shouldDownloadOnSide(obj["client_side"].toString()); + auto server = shouldDownloadOnSide(obj["server_side"].toString()); if (server && client) { - pack.side = "both"; + pack.side = ModPlatform::Side::UniversalSide; } else if (server) { - pack.side = "server"; + pack.side = ModPlatform::Side::ServerSide; } else if (client) { - pack.side = "client"; + pack.side = ModPlatform::Side::ClientSide; } // Modrinth can have more data than what's provided by the basic search :) @@ -76,61 +78,42 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.extraData.issuesUrl = Json::ensureString(obj, "issues_url"); + pack.extraData.issuesUrl = obj["issues_url"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); - pack.extraData.sourceUrl = Json::ensureString(obj, "source_url"); + pack.extraData.sourceUrl = obj["source_url"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); - pack.extraData.wikiUrl = Json::ensureString(obj, "wiki_url"); + pack.extraData.wikiUrl = obj["wiki_url"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); - pack.extraData.discordUrl = Json::ensureString(obj, "discord_url"); + pack.extraData.discordUrl = obj["discord_url"].toString(); if (pack.extraData.discordUrl.endsWith('/')) pack.extraData.discordUrl.chop(1); - auto donate_arr = Json::ensureArray(obj, "donation_urls"); + auto donate_arr = obj["donation_urls"].toArray(); for (auto d : donate_arr) { auto d_obj = Json::requireObject(d); ModPlatform::DonationData donate; - donate.id = Json::ensureString(d_obj, "id"); - donate.platform = Json::ensureString(d_obj, "platform"); - donate.url = Json::ensureString(d_obj, "url"); + donate.id = d_obj["id"].toString(); + donate.platform = d_obj["platform"].toString(); + donate.url = d_obj["url"].toString(); pack.extraData.donate.append(donate); } - pack.extraData.status = Json::ensureString(obj, "status"); + pack.extraData.status = obj["status"].toString(); - pack.extraData.body = Json::ensureString(obj, "body").remove("
    "); + pack.extraData.body = obj["body"].toString().remove("
    "); pack.extraDataLoaded = true; } -void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) -{ - QVector unsortedVersions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - auto file = loadIndexedPackVersion(obj); - - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid - unsortedVersions.append(file); - } - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); - pack.versions = unsortedVersions; - pack.versionsLoaded = true; -} - ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) { ModPlatform::IndexedVersion file; @@ -143,7 +126,8 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q return {}; } for (auto mcVer : versionArray) { - file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString())); + file.mcVersion.append({ ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()), + mcVer.toString() }); // double this so we can check both strings when filtering } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { @@ -162,16 +146,16 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); - file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); + file.version_type = ModPlatform::IndexedVersionType::fromString(Json::requireString(obj, "version_type")); file.changelog = Json::requireString(obj, "changelog"); - auto dependencies = Json::ensureArray(obj, "dependencies"); + auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { - auto dep = Json::ensureObject(d); + auto dep = d.toObject(); ModPlatform::Dependency dependency; - dependency.addonId = Json::ensureString(dep, "project_id"); - dependency.version = Json::ensureString(dep, "version_id"); + dependency.addonId = dep["project_id"].toString(); + dependency.version = dep["version_id"].toString(); auto depType = Json::requireString(dep, "dependency_type"); if (depType == "required") @@ -244,28 +228,3 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q return {}; } - -ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, - QJsonArray& arr, - const BaseInstance* inst) -{ - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - - QVector versions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - auto file = loadIndexedPackVersion(obj); - - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid - versions.append(file); - } - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(versions.begin(), versions.end(), orderSortPredicate); - return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion(); -} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 93f91eec2..5d852cb6f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -19,15 +19,12 @@ #include "modplatform/ModIndex.h" -#include #include "BaseInstance.h" namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp deleted file mode 100644 index 89ef6e4c4..000000000 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * 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 - * Copyright 2022 kb1000 - * - * 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 "ModrinthPackManifest.h" -#include -#include "Json.h" - -#include "modplatform/modrinth/ModrinthAPI.h" - -#include - -static ModrinthAPI api; - -namespace Modrinth { - -void loadIndexedPack(Modpack& pack, QJsonObject& obj) -{ - pack.id = Json::ensureString(obj, "project_id"); - - pack.name = Json::ensureString(obj, "title"); - pack.description = Json::ensureString(obj, "description"); - auto temp_author_name = Json::ensureString(obj, "author"); - pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name)); - pack.iconUrl = Json::ensureString(obj, "icon_url"); - pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix()); -} - -void loadIndexedInfo(Modpack& pack, QJsonObject& obj) -{ - pack.extra.body = Json::ensureString(obj, "body"); - pack.extra.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug")); - - pack.extra.issuesUrl = Json::ensureString(obj, "issues_url"); - if (pack.extra.issuesUrl.endsWith('/')) - pack.extra.issuesUrl.chop(1); - - pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); - if (pack.extra.sourceUrl.endsWith('/')) - pack.extra.sourceUrl.chop(1); - - pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); - if (pack.extra.wikiUrl.endsWith('/')) - pack.extra.wikiUrl.chop(1); - - pack.extra.discordUrl = Json::ensureString(obj, "discord_url"); - if (pack.extra.discordUrl.endsWith('/')) - pack.extra.discordUrl.chop(1); - - auto donate_arr = Json::ensureArray(obj, "donation_urls"); - for (auto d : donate_arr) { - auto d_obj = Json::requireObject(d); - - DonationData donate; - - donate.id = Json::ensureString(d_obj, "id"); - donate.platform = Json::ensureString(d_obj, "platform"); - donate.url = Json::ensureString(d_obj, "url"); - - pack.extra.donate.append(donate); - } - - pack.extra.status = Json::ensureString(obj, "status"); - - pack.extraInfoLoaded = true; -} - -void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) -{ - QVector unsortedVersions; - - auto arr = Json::requireArray(doc); - - for (auto versionIter : arr) { - auto obj = Json::requireObject(versionIter); - auto file = loadIndexedVersion(obj); - - if (!file.id.isEmpty()) // Heuristic to check if the returned value is valid - unsortedVersions.append(file); - } - auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - - std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); - - pack.versions.swap(unsortedVersions); - - pack.versionsLoaded = true; -} - -auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion -{ - ModpackVersion file; - - file.name = Json::requireString(obj, "name"); - file.version = Json::requireString(obj, "version_number"); - auto gameVersions = Json::ensureArray(obj, "game_versions"); - if (!gameVersions.isEmpty()) { - file.gameVersion = Json::ensureString(gameVersions[0]); - file.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion); - } - auto loaders = Json::requireArray(obj, "loaders"); - for (auto loader : loaders) { - if (loader == "neoforge") - file.loaders |= ModPlatform::NeoForge; - else if (loader == "forge") - file.loaders |= ModPlatform::Forge; - else if (loader == "cauldron") - file.loaders |= ModPlatform::Cauldron; - else if (loader == "liteloader") - file.loaders |= ModPlatform::LiteLoader; - else if (loader == "fabric") - file.loaders |= ModPlatform::Fabric; - else if (loader == "quilt") - file.loaders |= ModPlatform::Quilt; - } - file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); - file.changelog = Json::ensureString(obj, "changelog"); - - file.id = Json::requireString(obj, "id"); - file.project_id = Json::requireString(obj, "project_id"); - - file.date = Json::requireString(obj, "date_published"); - - auto files = Json::requireArray(obj, "files"); - - for (auto file_iter : files) { - File indexed_file; - auto parent = Json::requireObject(file_iter); - auto is_primary = Json::ensureBoolean(parent, (const QString)QStringLiteral("primary"), false); - if (!is_primary) { - auto filename = Json::ensureString(parent, "filename"); - // Checking suffix here is fine because it's the response from Modrinth, - // so one would assume it will always be in English. - if (!filename.endsWith("mrpack") && !filename.endsWith("zip")) - continue; - } - - auto url = Json::requireString(parent, "url"); - - file.download_url = url; - if (is_primary) - break; - } - - if (file.download_url.isEmpty()) - return {}; - - return file; -} - -} // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h deleted file mode 100644 index 2e5e2da84..000000000 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * 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 - * Copyright 2022 kb1000 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "modplatform/ModIndex.h" - -class MinecraftInstance; - -namespace Modrinth { - -struct File { - QString path; - - QCryptographicHash::Algorithm hashAlgorithm; - QByteArray hash; - QQueue downloads; - bool required = true; -}; - -struct DonationData { - QString id; - QString platform; - QString url; -}; - -struct ModpackExtra { - QString body; - - QString projectUrl; - - QString issuesUrl; - QString sourceUrl; - QString wikiUrl; - QString discordUrl; - - QList donate; - - QString status; -}; - -struct ModpackVersion { - QString name; - QString version; - QString gameVersion; - ModPlatform::IndexedVersionType version_type; - QString changelog; - ModPlatform::ModLoaderTypes loaders = {}; - - QString id; - QString project_id; - - QString date; - - QString download_url; -}; - -struct Modpack { - QString id; - - QString name; - QString description; - std::tuple author; - QString iconName; - QUrl iconUrl; - - bool versionsLoaded = false; - bool extraInfoLoaded = false; - - ModpackExtra extra; - QVector versions; -}; - -void loadIndexedPack(Modpack&, QJsonObject&); -void loadIndexedInfo(Modpack&, QJsonObject&); -void loadIndexedVersions(Modpack&, QJsonDocument&); -auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; - -auto validateDownloadUrl(QUrl) -> bool; - -} // namespace Modrinth - -Q_DECLARE_METATYPE(Modrinth::Modpack) -Q_DECLARE_METATYPE(Modrinth::ModpackVersion) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 04c8a926e..83ca0f4f2 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -28,14 +28,13 @@ #include "FileSystem.h" #include "StringUtils.h" -#include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" #include namespace Packwiz { -auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString +auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString { QFile index_file(index_dir.absoluteFilePath(normalized_fname)); @@ -72,7 +71,7 @@ auto stringEntry(toml::table table, QString entry_name) -> QString { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata."; + qDebug() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } @@ -83,14 +82,14 @@ auto intEntry(toml::table table, QString entry_name) -> int { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata."; + qDebug() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } return node.value_or(0); } -auto V1::createModFormat([[maybe_unused]] QDir& index_dir, +auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { @@ -113,28 +112,20 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; - mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side); + 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.releaseType = mod_version.version_type; - return mod; -} - -auto V1::createModFormat(QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, QString slug) -> Mod -{ - // Try getting metadata if it exists - Mod mod{ getIndexForMod(index_dir, slug) }; - if (mod.isValid()) - return mod; - - qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); + mod.version_number = mod_version.version_number; + if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number + mod.version_number = mod_version.version; - return {}; + return mod; } -void V1::updateModIndex(QDir& index_dir, Mod& mod) +void V1::updateModIndex(const QDir& index_dir, Mod& mod) { if (!mod.isValid()) { qCritical() << QString("Tried to update metadata of an invalid mod!"); @@ -204,10 +195,11 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) { auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, - { "side", sideToString(mod.side).toStdString() }, + { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() }, { "x-prismlauncher-loaders", loaders }, { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, + { "x-prismlauncher-version-number", mod.version_number.toStdString() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -225,7 +217,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) index_file.close(); } -void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) +void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) { auto normalized_fname = indexFileName(mod_slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); @@ -244,19 +236,7 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) } } -void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) -{ - for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { - auto mod = getIndexForMod(index_dir, file_name); - - if (mod.mod_id() == mod_id) { - deleteModIndex(index_dir, mod.name); - break; - } - } -} - -auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod +auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { Mod mod; @@ -291,8 +271,8 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); - mod.side = stringToSide(stringEntry(table, "side")); - mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "x-prismlauncher-release-type")); + mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side")); + mod.releaseType = ModPlatform::IndexedVersionType::fromString(table["x-prismlauncher-release-type"].value_or("")); if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { if (loader.is_string()) { @@ -312,6 +292,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.mcVersions.sort(); } } + mod.version_number = table["x-prismlauncher-version-number"].value_or(""); { // [download] info auto download_table = table["download"].as_table(); @@ -353,7 +334,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod return mod; } -auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod +auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod { for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { auto mod = getIndexForMod(index_dir, file_name); @@ -365,28 +346,4 @@ auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod return {}; } -auto V1::sideToString(Side side) -> QString -{ - switch (side) { - case Side::ClientSide: - return "client"; - case Side::ServerSide: - return "server"; - case Side::UniversalSide: - return "both"; - } - return {}; -} - -auto V1::stringToSide(QString side) -> Side -{ - if (side == "client") - return Side::ClientSide; - if (side == "server") - return Side::ServerSide; - if (side == "both") - return Side::UniversalSide; - return Side::UniversalSide; -} - } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 95362bbfe..ba9a0fe75 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -27,21 +27,18 @@ class QDir; -// Mod from launcher/minecraft/mod/Mod.h -class Mod; - namespace Packwiz { -auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; +auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; class V1 { public: - enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; + // can also represent other resources beside loader mods - but this is what packwiz calls it struct Mod { QString slug{}; QString name{}; QString filename{}; - Side side{ Side::UniversalSide }; + ModPlatform::Side side{ ModPlatform::Side::UniversalSide }; ModPlatform::ModLoaderTypes loaders; QStringList mcVersions; ModPlatform::IndexedVersionType releaseType; @@ -56,6 +53,7 @@ class V1 { ModPlatform::ResourceProvider provider{}; QVariant file_id{}; QVariant project_id{}; + QString version_number{}; public: // This is a totally heuristic, but should work for now. @@ -70,36 +68,26 @@ class V1 { /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher, when downloading mods. * */ - static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; - /* Generates the object representing the information in a mod.pw.toml file via - * its common representation in the launcher, plus a necessary slug. - * */ - static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; + static auto createModFormat(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. * */ - static void updateModIndex(QDir& index_dir, Mod& mod); + static void updateModIndex(const QDir& index_dir, Mod& mod); /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(QDir& index_dir, QString& mod_slug); - - /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(QDir& index_dir, QVariant& mod_id); + static void deleteModIndex(const QDir& index_dir, QString& mod_slug); /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod; + static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod; /* Gets the metadata for a mod with a particular id. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; - - static auto sideToString(Side side) -> QString; - static auto stringToSide(QString side) -> Side; + static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index cc9ced10b..09428c31d 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -66,11 +66,7 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it - m_packZip.reset(new QuaZip(m_archivePath)); - if (!m_packZip->open(QuaZip::mdUnzip)) { - emitFailed(tr("Unable to open supplied modpack zip file.")); - return; - } + m_packZip.reset(new MMCZip::ArchiveReader(m_archivePath)); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index d49d008b9..9dd54458d 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -16,10 +16,9 @@ #pragma once #include "InstanceTask.h" +#include "archive/ArchiveReader.h" #include "net/NetJob.h" -#include - #include #include #include @@ -54,7 +53,7 @@ class SingleZipPackInstallTask : public InstanceTask { QString m_minecraftVersion; QString m_archivePath; NetJob::Ptr m_filesNetJob; - std::unique_ptr m_packZip; + std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; }; diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index ffda05ee9..9d8a1a340 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,7 +72,7 @@ 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)); + m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response.get())); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h index 2ea701e23..ef319e6a5 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -46,7 +46,7 @@ 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, @@ -71,14 +71,14 @@ 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(); + std::unique_ptr m_response = std::make_unique(); QTemporaryDir m_outputDir; int m_modCount; QFuture m_extractFuture; diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp index 38b668f6b..4b9701e7d 100644 --- a/launcher/modplatform/technic/SolderPackManifest.cpp +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj) static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) { b.name = Json::requireString(obj, "name"); - b.version = Json::ensureString(obj, "version", ""); + b.version = obj["version"].toString(""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } diff --git a/launcher/modplatform/technic/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h index 1a06d7037..3a5947515 100644 --- a/launcher/modplatform/technic/SolderPackManifest.h +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -19,15 +19,15 @@ #pragma once #include +#include #include -#include namespace TechnicSolder { struct Pack { QString recommended; QString latest; - QVector builds; + QList builds; }; void loadPack(Pack& v, QJsonObject& obj); @@ -41,7 +41,7 @@ struct PackBuildMod { struct PackBuild { QString minecraft; - QVector mods; + QList mods; }; void loadPackBuild(PackBuild& v, QJsonObject& obj); diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 9050e14d8..e114398c4 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -19,14 +19,12 @@ #include #include #include -#include -#include -#include #include #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, @@ -35,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); @@ -53,35 +51,30 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); QString fmlMinecraftVersion; if (QFile::exists(modpackJar)) { - QuaZip zipFile(modpackJar); - if (!zipFile.open(QuaZip::mdUnzip)) { + MMCZip::ArchiveReader zipFile(modpackJar); + if (!zipFile.collectFiles()) { emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); return; } - QuaZipDir zipFileRoot(&zipFile, "/"); - if (zipFileRoot.exists("/version.json")) { - if (zipFileRoot.exists("/fmlversion.properties")) { - zipFile.setCurrentFile("fmlversion.properties"); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) { + if (zipFile.exists("/version.json")) { + if (zipFile.exists("/fmlversion.properties")) { + auto file = zipFile.goToFile("fmlversion.properties"); + if (!file) { emit failed(tr("Unable to open \"fmlversion.properties\"!")); return; } - QByteArray fmlVersionData = file.readAll(); - file.close(); + QByteArray fmlVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(fmlVersionData); // If not present, this evaluates to a null string fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); } - zipFile.setCurrentFile("version.json", QuaZip::csSensitive); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) { + auto file = zipFile.goToFile("version.json"); + if (!file) { emit failed(tr("Unable to open \"version.json\"!")); return; } - data = file.readAll(); - file.close(); + data = file->readAll(); } else { if (minecraftVersion.isEmpty()) { emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); @@ -93,16 +86,14 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, // Forge for 1.4.7 and for 1.5.2 require extra libraries. // Figure out the forge version and add it as a component // (the code still comes from the jar mod installed above) - if (zipFileRoot.exists("/forgeversion.properties")) { - zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); - QuaZipFile file(&zipFile); - if (!file.open(QIODevice::ReadOnly)) { + if (zipFile.exists("/forgeversion.properties")) { + auto file = zipFile.goToFile("forgeversion.properties"); + if (!file) { // Really shouldn't happen, but error handling shall not be forgotten emit failed(tr("Unable to open \"forgeversion.properties\"")); return; } - QByteArray forgeVersionData = file.readAll(); - file.close(); + auto forgeVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(forgeVersionData); QString major, minor, revision, build; @@ -142,7 +133,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, try { QJsonDocument doc = Json::requireDocument(data); QJsonObject root = Json::requireObject(doc, "version.json"); - QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + QString packMinecraftVersion = root["inheritsFrom"].toString(); if (packMinecraftVersion.isEmpty()) { if (fmlMinecraftVersion.isEmpty()) { emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); @@ -151,21 +142,21 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, packMinecraftVersion = fmlMinecraftVersion; } components->setComponentVersion("net.minecraft", packMinecraftVersion, true); - for (auto library : Json::ensureArray(root, "libraries", {})) { + for (auto library : root["libraries"].toArray()) { if (!library.isObject()) { continue; } - auto libraryObject = Json::ensureObject(library, {}, ""); - auto libraryName = Json::ensureString(libraryObject, "name", "", ""); + auto libraryObject = library.toObject(); + auto libraryName = libraryObject["name"].toString(); if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge // no easy way to get the version from the libs so use the arguments - auto arguments = Json::ensureObject(root, "arguments", {}); + auto arguments = root["arguments"].toObject(); bool isVersionArg = false; QString neoforgeVersion; - for (auto arg : Json::ensureArray(arguments, "game", {})) { - auto argument = Json::ensureString(arg, ""); + for (auto arg : arguments["game"].toArray()) { + auto argument = arg.toString(""); if (isVersionArg) { neoforgeVersion = argument; break; 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..c0e3d78a2 100644 --- a/launcher/net/ApiDownload.cpp +++ b/launcher/net/ApiDownload.cpp @@ -25,21 +25,21 @@ namespace Net { Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) { auto dl = Download::makeCached(url, entry, options); - dl->addHeaderProxy(new ApiHeaderProxy()); + dl->addHeaderProxy(std::make_unique()); return dl; } -Download::Ptr ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Download::Options options) +Download::Ptr ApiDownload::makeByteArray(QUrl url, QByteArray* output, Download::Options options) { auto dl = Download::makeByteArray(url, output, options); - dl->addHeaderProxy(new ApiHeaderProxy()); + dl->addHeaderProxy(std::make_unique()); return dl; } Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) { auto dl = Download::makeFile(url, path, options); - dl->addHeaderProxy(new ApiHeaderProxy()); + dl->addHeaderProxy(std::make_unique()); return dl; } diff --git a/launcher/net/ApiDownload.h b/launcher/net/ApiDownload.h index 842c25c56..25d2447e3 100644 --- a/launcher/net/ApiDownload.h +++ b/launcher/net/ApiDownload.h @@ -25,7 +25,7 @@ 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 makeByteArray(QUrl url, QByteArray* output, Download::Options options = Download::Option::NoOptions); Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); }; // namespace ApiDownload diff --git a/launcher/net/ApiUpload.cpp b/launcher/net/ApiUpload.cpp index a2b8f357b..acb7d141f 100644 --- a/launcher/net/ApiUpload.cpp +++ b/launcher/net/ApiUpload.cpp @@ -22,10 +22,10 @@ namespace Net { -Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +Upload::Ptr ApiUpload::makeByteArray(QUrl url, QByteArray* output, QByteArray m_post_data) { auto up = Upload::makeByteArray(url, output, m_post_data); - up->addHeaderProxy(new ApiHeaderProxy()); + up->addHeaderProxy(std::make_unique()); return up; } diff --git a/launcher/net/ApiUpload.h b/launcher/net/ApiUpload.h index 674a3b93f..b473c81c8 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); +Upload::Ptr makeByteArray(QUrl url, QByteArray* output, QByteArray m_post_data); }; } // namespace Net diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index ac64052b9..82e3f6592 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -45,7 +45,7 @@ namespace Net { */ class ByteArraySink : public Sink { public: - ByteArraySink(std::shared_ptr output) : m_output(output) {}; + ByteArraySink(QByteArray* output) : m_output(output) {} virtual ~ByteArraySink() = default; @@ -58,6 +58,7 @@ class ByteArraySink : public Sink { qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; if (initAllValidators(request)) return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; }; @@ -69,12 +70,14 @@ class ByteArraySink : public Sink { qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; if (writeAllValidators(data)) return Task::State::Running; + m_fail_reason = "Failed to write validators"; return Task::State::Failed; } auto abort() -> Task::State override { failAllValidators(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -82,12 +85,13 @@ class ByteArraySink : public Sink { { if (finalizeAllValidators(reply)) return Task::State::Succeeded; + m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } auto hasLocalData() -> bool override { return false; } - private: - std::shared_ptr m_output; + protected: + QByteArray* m_output; }; } // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 49686db98..8b7bef2d4 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -63,7 +63,7 @@ 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, QByteArray* output, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 5f6a5caf1..a571e5b03 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -54,7 +54,7 @@ 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; + static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr; 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 3a58a4667..1f519708f 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -51,6 +51,7 @@ Task::State FileSink::init(QNetworkRequest& request) // create a new save file and open it for writing if (!FS::ensureFilePathExists(m_filename)) { qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; + m_fail_reason = "Could not create folder"; return Task::State::Failed; } @@ -58,11 +59,13 @@ Task::State FileSink::init(QNetworkRequest& request) 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"; return Task::State::Failed; } if (initAllValidators(request)) return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; } @@ -73,6 +76,7 @@ Task::State FileSink::write(QByteArray& data) m_output_file->cancelWriting(); m_output_file.reset(); m_wroteAnyData = false; + m_fail_reason = "Failed to write validators"; return Task::State::Failed; } @@ -105,13 +109,16 @@ Task::State FileSink::finalize(QNetworkReply& reply) if (gotFile || m_wroteAnyData) { // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits - if (!finalizeAllValidators(reply)) + if (!finalizeAllValidators(reply)) { + m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; + } // nothing went wrong... if (!m_output_file->commit()) { qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename; 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 4985ad080..624f45ccf 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -112,7 +112,10 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); - input.open(QIODevice::ReadOnly); + if (!input.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file '" << input.fileName() << "' for reading!"; + return staleEntry(base, resource_path); + } QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); if (entry->m_md5sum != md5sum) { selected_base.entry_list.remove(resource_path); @@ -166,8 +169,10 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool return true; } -void HttpMetaCache::evictAll() +// returns true on success, false otherwise +auto HttpMetaCache::evictAll() -> bool { + bool ret = true; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; @@ -176,8 +181,10 @@ void HttpMetaCache::evictAll() qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); - FS::deletePath(map.base_path); + // AND all return codes together so the result is true iff all runs of deletePath() are true + ret &= FS::deletePath(map.base_path); } + return ret; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr @@ -241,15 +248,15 @@ void HttpMetaCache::Load() auto root = json.object(); // check file version first - auto version_val = Json::ensureString(root, "version"); + auto version_val = root["version"].toString(); if (version_val != "1") return; // read the entry array - auto array = Json::ensureArray(root, "entries"); + auto array = root["entries"].toArray(); for (auto element : array) { - auto element_obj = Json::ensureObject(element); - auto base = Json::ensureString(element_obj, "base"); + auto element_obj = element.toObject(); + auto base = element_obj["base"].toString(); if (!m_entries.contains(base)) continue; @@ -257,16 +264,16 @@ void HttpMetaCache::Load() auto foo = new MetaEntry(); foo->m_baseId = base; - foo->m_relativePath = Json::ensureString(element_obj, "path"); - foo->m_md5sum = Json::ensureString(element_obj, "md5sum"); - foo->m_etag = Json::ensureString(element_obj, "etag"); - foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); - foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); + foo->m_relativePath = element_obj["path"].toString(); + foo->m_md5sum = element_obj["md5sum"].toString(); + foo->m_etag = element_obj["etag"].toString(); + foo->m_local_changed_timestamp = element_obj["last_changed_timestamp"].toDouble(); + foo->m_remote_changed_timestamp = element_obj["remote_changed_timestamp"].toString(); - foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false)); + foo->makeEternal(element_obj[QStringLiteral("eternal")].toBool()); if (!foo->isEternal()) { - foo->m_current_age = Json::ensureDouble(element_obj, "current_age"); - foo->m_max_age = Json::ensureDouble(element_obj, "max_age"); + foo->m_current_age = element_obj["current_age"].toDouble(); + foo->m_max_age = element_obj["max_age"].toDouble(); } // presumed innocent until closer examination diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 036a8dd94..c8b02dae4 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -66,7 +66,7 @@ class MetaEntry { /* Whether the entry expires after some time (false) or not (true). */ void makeEternal(bool eternal) { m_is_eternal = eternal; } - [[nodiscard]] bool isEternal() const { return m_is_eternal; } + bool isEternal() const { return m_is_eternal; } auto getCurrentAge() -> qint64 { return m_current_age; } void setCurrentAge(qint64 age) { m_current_age = age; } @@ -113,7 +113,7 @@ class HttpMetaCache : public QObject { // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; - void evictAll(); + bool evictAll(); void addBase(QString base, QString base_root); diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 432c0c84b..8896f10e3 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -98,8 +98,8 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) auto cache_control_header = reply.rawHeader("Cache-Control"); qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; - QRegularExpression max_age_expr("max-age=([0-9]+)"); - qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong(); + static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); + qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); m_entry->setMaximumAge(max_age); } else if (reply.hasRawHeader("Expires")) { diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 335e360b2..444985431 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -44,8 +44,7 @@ #include "ui/dialogs/CustomMessageBox.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) 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 310653508..4544fb89c 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -84,7 +84,8 @@ void NetRequest::executeTask() break; case State::Inactive: case State::Failed: - emit failed("Failed to initialize sink"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; case State::AbortedByUser: @@ -104,12 +105,10 @@ void NetRequest::executeTask() header_proxy->writeHeaders(request); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if defined(LAUNCHER_APPLICATION) request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); #else request.setTransferTimeout(); -#endif #endif m_last_progress_time = m_clock.now(); @@ -122,11 +121,7 @@ void NetRequest::executeTask() connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &NetRequest::downloadError); -#endif connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } @@ -174,9 +169,11 @@ void NetRequest::downloadError(QNetworkReply::NetworkError error) } } // error happened during download. - qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with reason" << error; + qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with error" << error; if (m_reply) - qCCritical(logCat) << getUid().toString() << "HTTP Status" << replyStatusCode() << ";error" << errorString(); + qCCritical(logCat) << getUid().toString() << "HTTP status:" << replyStatusCode() << errorString(); + if (m_errorResponse.size() > 0) + qCCritical(logCat) << getUid().toString() << "Response from server:" << m_errorResponse; m_state = State::Failed; } } @@ -265,6 +262,7 @@ void NetRequest::downloadFinished() } else if (m_state == State::Failed) { qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); m_sink->abort(); + m_failReason = m_reply->errorString(); emit failed(m_reply->errorString()); emit finished(); return; @@ -284,7 +282,8 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); m_sink->abort(); - emit failed("failed to write in sink"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; } @@ -295,7 +294,8 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); m_sink->abort(); - emit failed("failed to finalize the request"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; } @@ -310,8 +310,11 @@ void NetRequest::downloadReadyRead() if (m_state == State::Running) { auto data = m_reply->readAll(); m_state = m_sink->write(data); + if (replyStatusCode() >= 400) { + m_errorResponse.append(data); + } if (m_state == State::Failed) { - qCCritical(logCat) << getUid().toString() << "Failed to process response chunk"; + qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason(); } // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; } else { @@ -323,11 +326,7 @@ auto NetRequest::abort() -> bool { m_state = State::AbortedByUser; if (m_reply) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); -#else - disconnect(m_reply.get(), QOverload::of(&QNetworkReply::error), nullptr, nullptr); -#endif m_reply->abort(); } return true; diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index 6b3f643d6..bdab9d68f 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -39,7 +39,6 @@ #pragma once -#include #include #include #include @@ -69,8 +68,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)); } QUrl url() const; void setUrl(QUrl url) { m_url = url; } @@ -101,14 +100,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; }; } // namespace Net diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index c67d3b23c..672637685 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -36,74 +36,44 @@ */ #include "PasteUpload.h" -#include "Application.h" -#include "BuildConfig.h" -#include -#include #include #include #include #include +#include #include +#include "logs/AnonymizeLog.h" -#include "net/Logging.h" +const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, + { "hastebin", "https://hst.sh", "/documents" }, + { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, + { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; -std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, - { "hastebin", "https://hst.sh", "/documents" }, - { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, - { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; - -PasteUpload::PasteUpload(QWidget* window, QString text, QString baseUrl, PasteType pasteType) - : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8()) +QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) { - if (m_baseUrl == "") - m_baseUrl = PasteTypes.at(pasteType).defaultBase; - - // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? - if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase) - m_uploadUrl = "https://api.paste.gg/v1/pastes"; - else - m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath; -} - -PasteUpload::~PasteUpload() {} - -void PasteUpload::executeTask() -{ - QNetworkRequest request{ QUrl(m_uploadUrl) }; - QNetworkReply* rep{}; - - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - - switch (m_pasteType) { - case NullPointer: { - QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType }; + switch (m_paste_type) { + case PasteUpload::NullPointer: { + QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; QHttpPart filePart; - filePart.setBody(m_text); + filePart.setBody(m_log.toUtf8()); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); multiPart->append(filePart); - rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); - - break; + return m_network->post(request, multiPart); } - case Hastebin: { - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - rep = APPLICATION->network()->post(request, m_text); - break; + case PasteUpload::Hastebin: { + return m_network->post(request, m_log.toUtf8()); } - case Mclogs: { + case PasteUpload::Mclogs: { QUrlQuery postData; - postData.addQueryItem("content", m_text); + postData.addQueryItem("content", m_log); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); - break; + return m_network->post(request, postData.toString().toUtf8()); } - case PasteGG: { + case PasteUpload::PasteGG: { QJsonObject obj; QJsonDocument doc; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -114,7 +84,7 @@ void PasteUpload::executeTask() QJsonObject logFileInfo; QJsonObject logFileContentInfo; logFileContentInfo.insert("format", "text"); - logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileContentInfo.insert("value", m_log); logFileInfo.insert("name", "log.txt"); logFileInfo.insert("content", logFileContentInfo); files.append(logFileInfo); @@ -122,112 +92,127 @@ void PasteUpload::executeTask() obj.insert("files", files); doc.setObject(obj); - rep = APPLICATION->network()->post(request, doc.toJson()); - break; + return m_network->post(request, doc.toJson()); } } - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &PasteUpload::downloadError); -#endif - - m_reply = std::shared_ptr(rep); + return nullptr; +}; - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); -} - -void PasteUpload::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error; - emitFailed(m_reply->errorString()); -} - -void PasteUpload::downloadFinished() +auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State { - QByteArray data = m_reply->readAll(); - int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (!finalizeAllValidators(reply)) { + m_fail_reason = "Failed to finalize validators"; + return Task::State::Failed; + } + int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(tr("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; + if (reply.error() != QNetworkReply::NetworkError::NoError) { + m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString()); + return Task::State::Failed; } else if (statusCode != 200 && statusCode != 201) { - QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode - << " with body: " << data; - m_reply.reset(); - return; + QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + m_fail_reason = + QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase); + return Task::State::Failed; } - switch (m_pasteType) { - case NullPointer: - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_d->m_paste_type) { + case PasteUpload::NullPointer: + m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed(); break; - case Hastebin: { - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("key") && jsonObj["key"].isString()) { - QString key = jsonDoc.object()["key"].toString(); - m_pasteLink = m_baseUrl + "/" + key; + case PasteUpload::Hastebin: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("key") && obj["key"].isString()) { + QString key = doc.object()["key"].toString(); + m_d->m_pasteLink = m_d->m_baseUrl + "/" + key; } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl - << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; } break; } - case Mclogs: { - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("success") && jsonObj["success"].isBool()) { - bool success = jsonObj["success"].toBool(); + case PasteUpload::Mclogs: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("success") && obj["success"].isBool()) { + bool success = obj["success"].toBool(); if (success) { - m_pasteLink = jsonObj["url"].toString(); + m_d->m_pasteLink = obj["url"].toString(); } else { - QString error = jsonObj["error"].toString(); - emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + QString error = obj["error"].toString(); + m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error); + return Task::State::Failed; } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; } break; } - case PasteGG: - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("status") && jsonObj["status"].isString()) { - QString status = jsonObj["status"].toString(); + case PasteUpload::PasteGG: + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("status") && obj["status"].isString()) { + QString status = obj["status"].toString(); if (status == "success") { - m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); + m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); } else { - QString error = jsonObj["error"].toString(); - QString message = - (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none"; - emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + QString error = obj["error"].toString(); + QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; + m_fail_reason = + QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message); + return Task::State::Failed; } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); + return Task::State::Failed; } break; } - emitSucceeded(); + return Task::State::Succeeded; +} + +PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType) +{ + anonymizeLog(m_log); + auto base = PasteUpload::PasteTypes.at(pasteType); + if (m_baseUrl.isEmpty()) + m_baseUrl = base.defaultBase; + + // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? + if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase) + m_url = "https://api.paste.gg/v1/pastes"; + else + m_url = m_baseUrl + base.endpointPath; + + m_sink.reset(new Sink(this, m_output.get())); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 2ba6067c3..cb0d2d5c1 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -35,15 +35,18 @@ #pragma once -#include +#include "net/ByteArraySink.h" +#include "net/NetRequest.h" +#include "tasks/Task.h" + #include +#include #include + #include #include -#include "tasks/Task.h" -class PasteUpload : public Task { - Q_OBJECT +class PasteUpload : public Net::NetRequest { public: enum PasteType : int { // 0x0.st @@ -58,32 +61,37 @@ class PasteUpload : public Task { First = NullPointer, Last = Mclogs }; - struct PasteTypeInfo { const QString name; const QString defaultBase; const QString endpointPath; }; - static std::array PasteTypes; + static const std::array PasteTypes; - PasteUpload(QWidget* window, QString text, QString url, PasteType pasteType); - virtual ~PasteUpload(); + class Sink : public Net::ByteArraySink { + public: + Sink(PasteUpload* p, QByteArray* output) : Net::ByteArraySink(output), m_d(p) {}; + virtual ~Sink() = default; - QString pasteLink() { return m_pasteLink; } + public: + auto finalize(QNetworkReply& reply) -> Task::State override; + + private: + PasteUpload* m_d; + }; + friend Sink; - protected: - virtual void executeTask(); + PasteUpload(const QString& log, QString url, PasteType pasteType); + virtual ~PasteUpload() = default; + + QString pasteLink() { return m_pasteLink; } private: - QWidget* m_window; + virtual QNetworkReply* getReply(QNetworkRequest&) override; + QString m_log; QString m_pasteLink; QString m_baseUrl; - QString m_uploadUrl; - PasteType m_pasteType; - QByteArray m_text; - std::shared_ptr m_reply; - public slots: - void downloadError(QNetworkReply::NetworkError); - void downloadFinished(); + const PasteType m_paste_type; + std::unique_ptr m_output = std::make_unique(); }; diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index d1fd9de10..3f04cbd82 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -52,6 +52,8 @@ class Sink { virtual auto hasLocalData() -> bool = 0; + QString failReason() const { return m_fail_reason; } + void addValidator(Validator* validator) { if (validator) { @@ -95,5 +97,6 @@ class Sink { protected: std::vector> validators; + QString m_fail_reason; }; } // namespace Net diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 623ec80f4..d162051f8 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -51,7 +51,7 @@ 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) +Upload::Ptr Upload::makeByteArray(QUrl url, QByteArray* output, QByteArray m_post_data) { auto up = makeShared(); up->m_url = std::move(url); diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index f920e5561..97db89250 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -47,7 +47,7 @@ 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); + static Upload::Ptr makeByteArray(QUrl url, QByteArray* output, QByteArray m_post_data); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 169589f78..72833c6d8 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -40,7 +40,7 @@ #include -NewsChecker::NewsChecker(shared_qobject_ptr network, const QString& feedUrl) +NewsChecker::NewsChecker(QNetworkAccessManager* network, const QString& feedUrl) { m_network = network; m_feedUrl = feedUrl; @@ -57,10 +57,10 @@ void NewsChecker::reloadNews() 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::makeByteArray(m_feedUrl, newsData.get())); job->setAskRetry(false); - QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); - QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); job->start(); } diff --git a/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h index cdd621a20..386bfd5ec 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,7 @@ class NewsChecker : public QObject { //! True if news has been loaded. bool m_loadedNews; - std::shared_ptr newsData = std::make_shared(); + std::unique_ptr newsData = std::make_unique(); /*! * Gets the error message that was given last time the news was loaded. @@ -92,7 +92,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/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h deleted file mode 100644 index 689f11979..000000000 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include -#include "IPathMatcher.h" - -class FSTreeMatcher : public IPathMatcher { - public: - virtual ~FSTreeMatcher() {}; - FSTreeMatcher(SeparatorPrefixTree<'/'>& tree) : m_fsTree(tree) {} - - bool matches(const QString& string) const override { return m_fsTree.covers(string); } - - SeparatorPrefixTree<'/'>& m_fsTree; -}; diff --git a/launcher/pathmatcher/IPathMatcher.h b/launcher/pathmatcher/IPathMatcher.h deleted file mode 100644 index f3b01e8cf..000000000 --- a/launcher/pathmatcher/IPathMatcher.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include -#include - -class IPathMatcher { - public: - using Ptr = std::shared_ptr; - - public: - virtual ~IPathMatcher() {} - virtual bool matches(const QString& string) const = 0; -}; diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h deleted file mode 100644 index 3e2bdb95d..000000000 --- a/launcher/pathmatcher/MultiMatcher.h +++ /dev/null @@ -1,26 +0,0 @@ -#include -#include -#include "IPathMatcher.h" - -class MultiMatcher : public IPathMatcher { - public: - virtual ~MultiMatcher() {}; - MultiMatcher() {} - MultiMatcher& add(Ptr add) - { - m_matchers.append(add); - return *this; - } - - virtual bool matches(const QString& string) const override - { - for (auto iter : m_matchers) { - if (iter->matches(string)) { - return true; - } - } - return false; - } - - QList m_matchers; -}; diff --git a/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h deleted file mode 100644 index a6a3e616d..000000000 --- a/launcher/pathmatcher/RegexpMatcher.h +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include "IPathMatcher.h" - -class RegexpMatcher : public IPathMatcher { - public: - virtual ~RegexpMatcher() {} - RegexpMatcher(const QString& regexp) - { - m_regexp.setPattern(regexp); - m_onlyFilenamePart = !regexp.contains('/'); - } - - RegexpMatcher& caseSensitive(bool cs = true) - { - if (cs) { - m_regexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); - } else { - m_regexp.setPatternOptions(QRegularExpression::NoPatternOption); - } - return *this; - } - - virtual bool matches(const QString& string) const override - { - if (m_onlyFilenamePart) { - auto slash = string.lastIndexOf('/'); - if (slash != -1) { - auto part = string.mid(slash + 1); - return m_regexp.match(part).hasMatch(); - } - } - return m_regexp.match(string).hasMatch(); - } - QRegularExpression m_regexp; - bool m_onlyFilenamePart = false; -}; diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h deleted file mode 100644 index ff3805179..000000000 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu -// -// SPDX-License-Identifier: GPL-3.0-only - -#include -#include "IPathMatcher.h" - -class SimplePrefixMatcher : public IPathMatcher { - public: - virtual ~SimplePrefixMatcher() {}; - SimplePrefixMatcher(const QString& prefix) - { - m_prefix = prefix; - m_isPrefix = prefix.endsWith('/'); - } - - virtual bool matches(const QString& string) const override - { - if (m_isPrefix) - return string.startsWith(m_prefix); - return string == m_prefix; - } - QString m_prefix; - bool m_isPrefix = false; -}; diff --git a/launcher/qtlogging.ini b/launcher/qtlogging.ini index c12d1e109..10f724163 100644 --- a/launcher/qtlogging.ini +++ b/launcher/qtlogging.ini @@ -3,6 +3,9 @@ # prevent log spam and strange bugs # qt.qpa.drawing in particular causes theme artifacts on MacOS qt.*.debug=false +# supress image format noise +kf.imageformats.plugins.hdr=false +kf.imageformats.plugins.xcf=false # don't log credentials by default launcher.auth.credentials.debug=false # remove the debug lines, other log levels still get through diff --git a/launcher/resources/assets/underconstruction.png b/launcher/resources/assets/underconstruction.png index 6ae06476e..5f2fdf9e4 100644 Binary files a/launcher/resources/assets/underconstruction.png and b/launcher/resources/assets/underconstruction.png differ diff --git a/launcher/resources/backgrounds/kitteh-bday.png b/launcher/resources/backgrounds/kitteh-bday.png index 09a365669..f4a7bbc1f 100644 Binary files a/launcher/resources/backgrounds/kitteh-bday.png and b/launcher/resources/backgrounds/kitteh-bday.png differ diff --git a/launcher/resources/backgrounds/kitteh-spooky.png b/launcher/resources/backgrounds/kitteh-spooky.png index deb0bebbe..bb3765f92 100644 Binary files a/launcher/resources/backgrounds/kitteh-spooky.png and b/launcher/resources/backgrounds/kitteh-spooky.png differ diff --git a/launcher/resources/backgrounds/kitteh-xmas.png b/launcher/resources/backgrounds/kitteh-xmas.png index 8bdb1d5c8..1e92e9081 100644 Binary files a/launcher/resources/backgrounds/kitteh-xmas.png and b/launcher/resources/backgrounds/kitteh-xmas.png differ diff --git a/launcher/resources/backgrounds/kitteh.png b/launcher/resources/backgrounds/kitteh.png index e9de7f27c..fa3d52548 100644 Binary files a/launcher/resources/backgrounds/kitteh.png and b/launcher/resources/backgrounds/kitteh.png differ diff --git a/launcher/resources/backgrounds/rory-bday.png b/launcher/resources/backgrounds/rory-bday.png index 66b880948..8c796927c 100644 Binary files a/launcher/resources/backgrounds/rory-bday.png and b/launcher/resources/backgrounds/rory-bday.png differ diff --git a/launcher/resources/backgrounds/rory-flat-bday.png b/launcher/resources/backgrounds/rory-flat-bday.png index 8a6e366db..94c4509a4 100644 Binary files a/launcher/resources/backgrounds/rory-flat-bday.png and b/launcher/resources/backgrounds/rory-flat-bday.png differ diff --git a/launcher/resources/backgrounds/rory-flat-spooky.png b/launcher/resources/backgrounds/rory-flat-spooky.png index 6360c612f..4a0046c2b 100644 Binary files a/launcher/resources/backgrounds/rory-flat-spooky.png and b/launcher/resources/backgrounds/rory-flat-spooky.png differ diff --git a/launcher/resources/backgrounds/rory-flat-xmas.png b/launcher/resources/backgrounds/rory-flat-xmas.png index 96c3ae381..e6278ed5c 100644 Binary files a/launcher/resources/backgrounds/rory-flat-xmas.png and b/launcher/resources/backgrounds/rory-flat-xmas.png differ diff --git a/launcher/resources/backgrounds/rory-flat.png b/launcher/resources/backgrounds/rory-flat.png index ccec0662b..22fe61887 100644 Binary files a/launcher/resources/backgrounds/rory-flat.png and b/launcher/resources/backgrounds/rory-flat.png differ diff --git a/launcher/resources/backgrounds/rory-spooky.png b/launcher/resources/backgrounds/rory-spooky.png index a727619b4..1aa928671 100644 Binary files a/launcher/resources/backgrounds/rory-spooky.png and b/launcher/resources/backgrounds/rory-spooky.png differ diff --git a/launcher/resources/backgrounds/rory-xmas.png b/launcher/resources/backgrounds/rory-xmas.png index 107feb780..f33e92666 100644 Binary files a/launcher/resources/backgrounds/rory-xmas.png and b/launcher/resources/backgrounds/rory-xmas.png differ diff --git a/launcher/resources/backgrounds/rory.png b/launcher/resources/backgrounds/rory.png index 577f4dce9..5570499c2 100644 Binary files a/launcher/resources/backgrounds/rory.png and b/launcher/resources/backgrounds/rory.png differ diff --git a/launcher/resources/backgrounds/teawie-bday.png b/launcher/resources/backgrounds/teawie-bday.png index f4ecf247c..b4621f9b5 100644 Binary files a/launcher/resources/backgrounds/teawie-bday.png and b/launcher/resources/backgrounds/teawie-bday.png differ diff --git a/launcher/resources/backgrounds/teawie-spooky.png b/launcher/resources/backgrounds/teawie-spooky.png index cefc6c855..194d8ab7c 100644 Binary files a/launcher/resources/backgrounds/teawie-spooky.png and b/launcher/resources/backgrounds/teawie-spooky.png differ diff --git a/launcher/resources/backgrounds/teawie-xmas.png b/launcher/resources/backgrounds/teawie-xmas.png index 55fb7cfc6..54a09ae51 100644 Binary files a/launcher/resources/backgrounds/teawie-xmas.png and b/launcher/resources/backgrounds/teawie-xmas.png differ diff --git a/launcher/resources/backgrounds/teawie.png b/launcher/resources/backgrounds/teawie.png index dc32c51f9..99b60ad1e 100644 Binary files a/launcher/resources/backgrounds/teawie.png and b/launcher/resources/backgrounds/teawie.png differ diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc index 61d82ec30..585f2c60a 100644 --- a/launcher/resources/breeze_dark/breeze_dark.qrc +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg @@ -43,5 +43,6 @@ scalable/rename.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/breeze_dark/scalable/appearance.svg b/launcher/resources/breeze_dark/scalable/appearance.svg new file mode 100644 index 000000000..93e6ffa76 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/environment-variables.svg b/launcher/resources/breeze_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_dark/scalable/environment-variables.svg rename to launcher/resources/breeze_dark/scalable/datapacks.svg diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc index 2211c7188..2b0adba10 100644 --- a/launcher/resources/breeze_light/breeze_light.qrc +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg @@ -43,5 +43,6 @@ scalable/rename.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/breeze_light/scalable/appearance.svg b/launcher/resources/breeze_light/scalable/appearance.svg new file mode 100644 index 000000000..6e6d64a79 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/environment-variables.svg b/launcher/resources/breeze_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_light/scalable/environment-variables.svg rename to launcher/resources/breeze_light/scalable/datapacks.svg diff --git a/launcher/resources/documents/credits.html b/launcher/resources/documents/credits.html new file mode 100644 index 000000000..e86cdec5f --- /dev/null +++ b/launcher/resources/documents/credits.html @@ -0,0 +1,44 @@ +
    +

    %1

    +

    sogik <GitHub>

    +
    +

    %2

    +

    Sefa Eyeoglu (Scrumplex) <Website>

    +

    d-513 <GitHub>

    +

    txtsd <Website>

    +

    timoreo <GitHub>

    +

    ZekeZ <GitHub>

    +

    cozyGalvinism <GitHub>

    +

    DioEgizio <GitHub>

    +

    flowln <GitHub>

    +

    ViRb3 <GitHub>

    +

    Rachel Powers (Ryex) <GitHub>

    +

    TayouVR <GitHub>

    +

    TheKodeToad <GitHub>

    +

    getchoo <GitHub>

    +

    Alexandru Tripon (Trial97) <GitHub>

    +
    +

    %3

    +

    Andrew Okin <forkk@forkk.net>

    +

    Petr Mrázek <peterix@gmail.com>

    +

    Sky Welch <multimc@bunnies.io>

    +

    Jan (02JanDal) <02jandal@gmail.com>

    +

    RoboSky <@RoboSky_>

    +
    +

    %4

    +

    Boba <Website>

    +

    AutiOne <Website>

    +

    Fulmine <Website>

    +

    ely <GitHub>

    +

    gon sawa <GitHub>

    +

    Pankakes

    +

    tobimori <GitHub>

    +

    Orochimarufan <orochimarufan.x3@gmail.com>

    +

    TakSuyu <taksuyu@gmail.com>

    +

    Kilobyte <stiepen22@gmx.de>

    +

    Rootbear75 <@rootbear75>

    +

    Zeker Zhayard <@Zeker_Zhayard>

    +

    Everyone who helped establish our branding!

    +

    And everyone else who contributed!

    +
    +
    diff --git a/launcher/resources/documents/documents.qrc b/launcher/resources/documents/documents.qrc index 489d1d5a2..f4202c8dd 100644 --- a/launcher/resources/documents/documents.qrc +++ b/launcher/resources/documents/documents.qrc @@ -2,7 +2,7 @@ ../../../COPYING.md - login-qr.svg + credits.html diff --git a/launcher/resources/documents/login-qr.svg b/launcher/resources/documents/login-qr.svg deleted file mode 100644 index 1b88e3c83..000000000 --- a/launcher/resources/documents/login-qr.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index 8876027da..2cc9f46f5 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg @@ -49,5 +49,6 @@ scalable/rename.svg scalable/server.svg scalable/launch.svg + scalable/appearance.svg diff --git a/launcher/resources/flat/scalable/appearance.svg b/launcher/resources/flat/scalable/appearance.svg new file mode 100644 index 000000000..11dcb3f33 --- /dev/null +++ b/launcher/resources/flat/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/flat/scalable/environment-variables.svg b/launcher/resources/flat/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat/scalable/environment-variables.svg rename to launcher/resources/flat/scalable/datapacks.svg diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index 83b178cbf..b57c576f4 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg @@ -42,12 +42,13 @@ scalable/status-running.svg scalable/status-yellow.svg scalable/viewfolder.svg - scalable/worlds.svg + scalable/worlds.svg scalable/delete.svg scalable/export.svg scalable/rename.svg scalable/tag.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/flat_white/scalable/appearance.svg b/launcher/resources/flat_white/scalable/appearance.svg new file mode 100644 index 000000000..b20d91f12 --- /dev/null +++ b/launcher/resources/flat_white/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/flat_white/scalable/environment-variables.svg b/launcher/resources/flat_white/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat_white/scalable/environment-variables.svg rename to launcher/resources/flat_white/scalable/datapacks.svg diff --git a/launcher/resources/multimc/128x128/instances/chicken_legacy.png b/launcher/resources/multimc/128x128/instances/chicken_legacy.png index 71f6dedc5..b4945d75a 100644 Binary files a/launcher/resources/multimc/128x128/instances/chicken_legacy.png and b/launcher/resources/multimc/128x128/instances/chicken_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/creeper_legacy.png b/launcher/resources/multimc/128x128/instances/creeper_legacy.png index 41b7d07db..92d923132 100644 Binary files a/launcher/resources/multimc/128x128/instances/creeper_legacy.png and b/launcher/resources/multimc/128x128/instances/creeper_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png b/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png index 0a5bf91a4..fd910da47 100644 Binary files a/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png and b/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/flame_legacy.png b/launcher/resources/multimc/128x128/instances/flame_legacy.png index 6482975c4..3dd8500c6 100644 Binary files a/launcher/resources/multimc/128x128/instances/flame_legacy.png and b/launcher/resources/multimc/128x128/instances/flame_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/forge.png b/launcher/resources/multimc/128x128/instances/forge.png index d8ff79a53..10c5f8d6b 100644 Binary files a/launcher/resources/multimc/128x128/instances/forge.png and b/launcher/resources/multimc/128x128/instances/forge.png differ diff --git a/launcher/resources/multimc/128x128/instances/ftb_glow.png b/launcher/resources/multimc/128x128/instances/ftb_glow.png index 86632b21d..a8bfbbb96 100644 Binary files a/launcher/resources/multimc/128x128/instances/ftb_glow.png and b/launcher/resources/multimc/128x128/instances/ftb_glow.png differ diff --git a/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png b/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png index e725b7fe4..01aa4d517 100644 Binary files a/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png and b/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/gear_legacy.png b/launcher/resources/multimc/128x128/instances/gear_legacy.png index 75c68a66f..bb46fe026 100644 Binary files a/launcher/resources/multimc/128x128/instances/gear_legacy.png and b/launcher/resources/multimc/128x128/instances/gear_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/herobrine_legacy.png b/launcher/resources/multimc/128x128/instances/herobrine_legacy.png index 13f1494c4..d25d1b1b1 100644 Binary files a/launcher/resources/multimc/128x128/instances/herobrine_legacy.png and b/launcher/resources/multimc/128x128/instances/herobrine_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/infinity_legacy.png b/launcher/resources/multimc/128x128/instances/infinity_legacy.png index 63e06e5b3..322ab4361 100644 Binary files a/launcher/resources/multimc/128x128/instances/infinity_legacy.png and b/launcher/resources/multimc/128x128/instances/infinity_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/liteloader.png b/launcher/resources/multimc/128x128/instances/liteloader.png index 646217de0..acd977d7e 100644 Binary files a/launcher/resources/multimc/128x128/instances/liteloader.png and b/launcher/resources/multimc/128x128/instances/liteloader.png differ diff --git a/launcher/resources/multimc/128x128/instances/magitech_legacy.png b/launcher/resources/multimc/128x128/instances/magitech_legacy.png index 0f81a1997..c83d0c948 100644 Binary files a/launcher/resources/multimc/128x128/instances/magitech_legacy.png and b/launcher/resources/multimc/128x128/instances/magitech_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/meat_legacy.png b/launcher/resources/multimc/128x128/instances/meat_legacy.png index fefc9bf11..14a50bec0 100644 Binary files a/launcher/resources/multimc/128x128/instances/meat_legacy.png and b/launcher/resources/multimc/128x128/instances/meat_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/netherstar_legacy.png b/launcher/resources/multimc/128x128/instances/netherstar_legacy.png index 132085f02..86cc87b4a 100644 Binary files a/launcher/resources/multimc/128x128/instances/netherstar_legacy.png and b/launcher/resources/multimc/128x128/instances/netherstar_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/skeleton_legacy.png b/launcher/resources/multimc/128x128/instances/skeleton_legacy.png index 55fcf5a99..416ca66e0 100644 Binary files a/launcher/resources/multimc/128x128/instances/skeleton_legacy.png and b/launcher/resources/multimc/128x128/instances/skeleton_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png b/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png index c82d8406d..b7e2bdc13 100644 Binary files a/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png and b/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png differ diff --git a/launcher/resources/multimc/128x128/instances/steve_legacy.png b/launcher/resources/multimc/128x128/instances/steve_legacy.png index a07cbd2f9..afe8aaf46 100644 Binary files a/launcher/resources/multimc/128x128/instances/steve_legacy.png and b/launcher/resources/multimc/128x128/instances/steve_legacy.png differ diff --git a/launcher/resources/multimc/128x128/shaderpacks.png b/launcher/resources/multimc/128x128/shaderpacks.png index 1de0e9169..d2f1c0328 100644 Binary files a/launcher/resources/multimc/128x128/shaderpacks.png and b/launcher/resources/multimc/128x128/shaderpacks.png differ diff --git a/launcher/resources/multimc/128x128/unknown_server.png b/launcher/resources/multimc/128x128/unknown_server.png index ec98382d4..b9761e08f 100644 Binary files a/launcher/resources/multimc/128x128/unknown_server.png and b/launcher/resources/multimc/128x128/unknown_server.png differ diff --git a/launcher/resources/multimc/16x16/about.png b/launcher/resources/multimc/16x16/about.png index a6a986e19..ed7e56dd6 100644 Binary files a/launcher/resources/multimc/16x16/about.png and b/launcher/resources/multimc/16x16/about.png differ diff --git a/launcher/resources/multimc/16x16/bug.png b/launcher/resources/multimc/16x16/bug.png index 0c5b78b40..57e7d8203 100644 Binary files a/launcher/resources/multimc/16x16/bug.png and b/launcher/resources/multimc/16x16/bug.png differ diff --git a/launcher/resources/multimc/16x16/cat.png b/launcher/resources/multimc/16x16/cat.png index e6e31b44b..73d5fa856 100644 Binary files a/launcher/resources/multimc/16x16/cat.png and b/launcher/resources/multimc/16x16/cat.png differ diff --git a/launcher/resources/multimc/16x16/centralmods.png b/launcher/resources/multimc/16x16/centralmods.png index c1b91c763..0a573fb4e 100644 Binary files a/launcher/resources/multimc/16x16/centralmods.png and b/launcher/resources/multimc/16x16/centralmods.png differ diff --git a/launcher/resources/multimc/16x16/checkupdate.png b/launcher/resources/multimc/16x16/checkupdate.png index f37420588..9d08c56f0 100644 Binary files a/launcher/resources/multimc/16x16/checkupdate.png and b/launcher/resources/multimc/16x16/checkupdate.png differ diff --git a/launcher/resources/multimc/16x16/copy.png b/launcher/resources/multimc/16x16/copy.png index ccaed9e11..24251adcf 100644 Binary files a/launcher/resources/multimc/16x16/copy.png and b/launcher/resources/multimc/16x16/copy.png differ diff --git a/launcher/resources/multimc/16x16/coremods.png b/launcher/resources/multimc/16x16/coremods.png index af0f11667..3d3932dbe 100644 Binary files a/launcher/resources/multimc/16x16/coremods.png and b/launcher/resources/multimc/16x16/coremods.png differ diff --git a/launcher/resources/multimc/16x16/help.png b/launcher/resources/multimc/16x16/help.png index e6edf6ba2..3dee5a3f9 100644 Binary files a/launcher/resources/multimc/16x16/help.png and b/launcher/resources/multimc/16x16/help.png differ diff --git a/launcher/resources/multimc/16x16/instance-settings.png b/launcher/resources/multimc/16x16/instance-settings.png index b916cd245..6c9073b96 100644 Binary files a/launcher/resources/multimc/16x16/instance-settings.png and b/launcher/resources/multimc/16x16/instance-settings.png differ diff --git a/launcher/resources/multimc/16x16/jarmods.png b/launcher/resources/multimc/16x16/jarmods.png index 1a97c9c00..cdcbe788b 100644 Binary files a/launcher/resources/multimc/16x16/jarmods.png and b/launcher/resources/multimc/16x16/jarmods.png differ diff --git a/launcher/resources/multimc/16x16/loadermods.png b/launcher/resources/multimc/16x16/loadermods.png index b5ab3fced..ad0e6237d 100644 Binary files a/launcher/resources/multimc/16x16/loadermods.png and b/launcher/resources/multimc/16x16/loadermods.png differ diff --git a/launcher/resources/multimc/16x16/log.png b/launcher/resources/multimc/16x16/log.png index efa2a0b57..74324047e 100644 Binary files a/launcher/resources/multimc/16x16/log.png and b/launcher/resources/multimc/16x16/log.png differ diff --git a/launcher/resources/multimc/16x16/minecraft.png b/launcher/resources/multimc/16x16/minecraft.png index e9f2f2a5f..3de54f74a 100644 Binary files a/launcher/resources/multimc/16x16/minecraft.png and b/launcher/resources/multimc/16x16/minecraft.png differ diff --git a/launcher/resources/multimc/16x16/new.png b/launcher/resources/multimc/16x16/new.png index 2e56f5893..dfde06f61 100644 Binary files a/launcher/resources/multimc/16x16/new.png and b/launcher/resources/multimc/16x16/new.png differ diff --git a/launcher/resources/multimc/16x16/news.png b/launcher/resources/multimc/16x16/news.png index 872e85dbc..04e016da7 100644 Binary files a/launcher/resources/multimc/16x16/news.png and b/launcher/resources/multimc/16x16/news.png differ diff --git a/launcher/resources/multimc/16x16/noaccount.png b/launcher/resources/multimc/16x16/noaccount.png index b49bcf36a..544d68207 100644 Binary files a/launcher/resources/multimc/16x16/noaccount.png and b/launcher/resources/multimc/16x16/noaccount.png differ diff --git a/launcher/resources/multimc/16x16/patreon.png b/launcher/resources/multimc/16x16/patreon.png index 9150c478f..0c306e7cc 100644 Binary files a/launcher/resources/multimc/16x16/patreon.png and b/launcher/resources/multimc/16x16/patreon.png differ diff --git a/launcher/resources/multimc/16x16/refresh.png b/launcher/resources/multimc/16x16/refresh.png index 86b6f82c1..2e81c9246 100644 Binary files a/launcher/resources/multimc/16x16/refresh.png and b/launcher/resources/multimc/16x16/refresh.png differ diff --git a/launcher/resources/multimc/16x16/resourcepacks.png b/launcher/resources/multimc/16x16/resourcepacks.png index d862f5ca6..ac4c5dc43 100644 Binary files a/launcher/resources/multimc/16x16/resourcepacks.png and b/launcher/resources/multimc/16x16/resourcepacks.png differ diff --git a/launcher/resources/multimc/16x16/screenshots.png b/launcher/resources/multimc/16x16/screenshots.png index 460000d4b..f0e5e439e 100644 Binary files a/launcher/resources/multimc/16x16/screenshots.png and b/launcher/resources/multimc/16x16/screenshots.png differ diff --git a/launcher/resources/multimc/16x16/settings.png b/launcher/resources/multimc/16x16/settings.png index b916cd245..6c9073b96 100644 Binary files a/launcher/resources/multimc/16x16/settings.png and b/launcher/resources/multimc/16x16/settings.png differ diff --git a/launcher/resources/multimc/16x16/star.png b/launcher/resources/multimc/16x16/star.png index 4963e6ec9..20278be0c 100644 Binary files a/launcher/resources/multimc/16x16/star.png and b/launcher/resources/multimc/16x16/star.png differ diff --git a/launcher/resources/multimc/16x16/status-bad.png b/launcher/resources/multimc/16x16/status-bad.png index 5b3f20518..c71142b8a 100644 Binary files a/launcher/resources/multimc/16x16/status-bad.png and b/launcher/resources/multimc/16x16/status-bad.png differ diff --git a/launcher/resources/multimc/16x16/status-good.png b/launcher/resources/multimc/16x16/status-good.png index 5cbdee815..456a67c5b 100644 Binary files a/launcher/resources/multimc/16x16/status-good.png and b/launcher/resources/multimc/16x16/status-good.png differ diff --git a/launcher/resources/multimc/16x16/status-running.png b/launcher/resources/multimc/16x16/status-running.png index a4c42e392..7b7bfec91 100644 Binary files a/launcher/resources/multimc/16x16/status-running.png and b/launcher/resources/multimc/16x16/status-running.png differ diff --git a/launcher/resources/multimc/16x16/status-yellow.png b/launcher/resources/multimc/16x16/status-yellow.png index b25375d18..f652ddae2 100644 Binary files a/launcher/resources/multimc/16x16/status-yellow.png and b/launcher/resources/multimc/16x16/status-yellow.png differ diff --git a/launcher/resources/multimc/16x16/viewfolder.png b/launcher/resources/multimc/16x16/viewfolder.png index 98b8a9448..f5f401427 100644 Binary files a/launcher/resources/multimc/16x16/viewfolder.png and b/launcher/resources/multimc/16x16/viewfolder.png differ diff --git a/launcher/resources/multimc/16x16/worlds.png b/launcher/resources/multimc/16x16/worlds.png index 1a38f38e7..ed4249ec3 100644 Binary files a/launcher/resources/multimc/16x16/worlds.png and b/launcher/resources/multimc/16x16/worlds.png differ diff --git a/launcher/resources/multimc/22x22/about.png b/launcher/resources/multimc/22x22/about.png index 57775e25a..fbf18726f 100644 Binary files a/launcher/resources/multimc/22x22/about.png and b/launcher/resources/multimc/22x22/about.png differ diff --git a/launcher/resources/multimc/22x22/bug.png b/launcher/resources/multimc/22x22/bug.png index 90481bba6..8aeb25d66 100644 Binary files a/launcher/resources/multimc/22x22/bug.png and b/launcher/resources/multimc/22x22/bug.png differ diff --git a/launcher/resources/multimc/22x22/cat.png b/launcher/resources/multimc/22x22/cat.png index 3ea7ba69e..a5795b9b8 100644 Binary files a/launcher/resources/multimc/22x22/cat.png and b/launcher/resources/multimc/22x22/cat.png differ diff --git a/launcher/resources/multimc/22x22/centralmods.png b/launcher/resources/multimc/22x22/centralmods.png index a10f9a2b9..a54fdb0b0 100644 Binary files a/launcher/resources/multimc/22x22/centralmods.png and b/launcher/resources/multimc/22x22/centralmods.png differ diff --git a/launcher/resources/multimc/22x22/checkupdate.png b/launcher/resources/multimc/22x22/checkupdate.png index badb200cf..a44d47fe0 100644 Binary files a/launcher/resources/multimc/22x22/checkupdate.png and b/launcher/resources/multimc/22x22/checkupdate.png differ diff --git a/launcher/resources/multimc/22x22/copy.png b/launcher/resources/multimc/22x22/copy.png index ea236a241..5cdc69dbe 100644 Binary files a/launcher/resources/multimc/22x22/copy.png and b/launcher/resources/multimc/22x22/copy.png differ diff --git a/launcher/resources/multimc/22x22/help.png b/launcher/resources/multimc/22x22/help.png index da79b3e3f..db49f9e31 100644 Binary files a/launcher/resources/multimc/22x22/help.png and b/launcher/resources/multimc/22x22/help.png differ diff --git a/launcher/resources/multimc/22x22/instance-settings.png b/launcher/resources/multimc/22x22/instance-settings.png index daf56aad3..8eb9ee49d 100644 Binary files a/launcher/resources/multimc/22x22/instance-settings.png and b/launcher/resources/multimc/22x22/instance-settings.png differ diff --git a/launcher/resources/multimc/22x22/new.png b/launcher/resources/multimc/22x22/new.png index c707fbbfb..41edf3ef0 100644 Binary files a/launcher/resources/multimc/22x22/new.png and b/launcher/resources/multimc/22x22/new.png differ diff --git a/launcher/resources/multimc/22x22/news.png b/launcher/resources/multimc/22x22/news.png index 1953bf7b2..46eaab869 100644 Binary files a/launcher/resources/multimc/22x22/news.png and b/launcher/resources/multimc/22x22/news.png differ diff --git a/launcher/resources/multimc/22x22/patreon.png b/launcher/resources/multimc/22x22/patreon.png index f2c2076c0..8da8780ec 100644 Binary files a/launcher/resources/multimc/22x22/patreon.png and b/launcher/resources/multimc/22x22/patreon.png differ diff --git a/launcher/resources/multimc/22x22/refresh.png b/launcher/resources/multimc/22x22/refresh.png index 45b5535ce..f517f7ace 100644 Binary files a/launcher/resources/multimc/22x22/refresh.png and b/launcher/resources/multimc/22x22/refresh.png differ diff --git a/launcher/resources/multimc/22x22/screenshots.png b/launcher/resources/multimc/22x22/screenshots.png index 6fb42bbdf..780eb4351 100644 Binary files a/launcher/resources/multimc/22x22/screenshots.png and b/launcher/resources/multimc/22x22/screenshots.png differ diff --git a/launcher/resources/multimc/22x22/settings.png b/launcher/resources/multimc/22x22/settings.png index daf56aad3..8eb9ee49d 100644 Binary files a/launcher/resources/multimc/22x22/settings.png and b/launcher/resources/multimc/22x22/settings.png differ diff --git a/launcher/resources/multimc/22x22/status-bad.png b/launcher/resources/multimc/22x22/status-bad.png index 2707539e1..9d001ccd2 100644 Binary files a/launcher/resources/multimc/22x22/status-bad.png and b/launcher/resources/multimc/22x22/status-bad.png differ diff --git a/launcher/resources/multimc/22x22/status-good.png b/launcher/resources/multimc/22x22/status-good.png index f55debc37..9ac765abe 100644 Binary files a/launcher/resources/multimc/22x22/status-good.png and b/launcher/resources/multimc/22x22/status-good.png differ diff --git a/launcher/resources/multimc/22x22/status-running.png b/launcher/resources/multimc/22x22/status-running.png index 0dffba18f..21caa06b8 100644 Binary files a/launcher/resources/multimc/22x22/status-running.png and b/launcher/resources/multimc/22x22/status-running.png differ diff --git a/launcher/resources/multimc/22x22/status-yellow.png b/launcher/resources/multimc/22x22/status-yellow.png index 481eb7f3f..e125cf098 100644 Binary files a/launcher/resources/multimc/22x22/status-yellow.png and b/launcher/resources/multimc/22x22/status-yellow.png differ diff --git a/launcher/resources/multimc/22x22/viewfolder.png b/launcher/resources/multimc/22x22/viewfolder.png index b645167fc..7065e9ac9 100644 Binary files a/launcher/resources/multimc/22x22/viewfolder.png and b/launcher/resources/multimc/22x22/viewfolder.png differ diff --git a/launcher/resources/multimc/22x22/worlds.png b/launcher/resources/multimc/22x22/worlds.png index e8825bab4..ebb32f10c 100644 Binary files a/launcher/resources/multimc/22x22/worlds.png and b/launcher/resources/multimc/22x22/worlds.png differ diff --git a/launcher/resources/multimc/24x24/cat.png b/launcher/resources/multimc/24x24/cat.png index c93245f65..08b0ab1b0 100644 Binary files a/launcher/resources/multimc/24x24/cat.png and b/launcher/resources/multimc/24x24/cat.png differ diff --git a/launcher/resources/multimc/24x24/coremods.png b/launcher/resources/multimc/24x24/coremods.png index 90603d248..0cbd3f173 100644 Binary files a/launcher/resources/multimc/24x24/coremods.png and b/launcher/resources/multimc/24x24/coremods.png differ diff --git a/launcher/resources/multimc/24x24/jarmods.png b/launcher/resources/multimc/24x24/jarmods.png index 68cb8e9db..a4824c276 100644 Binary files a/launcher/resources/multimc/24x24/jarmods.png and b/launcher/resources/multimc/24x24/jarmods.png differ diff --git a/launcher/resources/multimc/24x24/loadermods.png b/launcher/resources/multimc/24x24/loadermods.png index 250a62609..cd4954d5a 100644 Binary files a/launcher/resources/multimc/24x24/loadermods.png and b/launcher/resources/multimc/24x24/loadermods.png differ diff --git a/launcher/resources/multimc/24x24/log.png b/launcher/resources/multimc/24x24/log.png index fe3020534..7978968c1 100644 Binary files a/launcher/resources/multimc/24x24/log.png and b/launcher/resources/multimc/24x24/log.png differ diff --git a/launcher/resources/multimc/24x24/minecraft.png b/launcher/resources/multimc/24x24/minecraft.png index b31177c9c..8869844cb 100644 Binary files a/launcher/resources/multimc/24x24/minecraft.png and b/launcher/resources/multimc/24x24/minecraft.png differ diff --git a/launcher/resources/multimc/24x24/noaccount.png b/launcher/resources/multimc/24x24/noaccount.png index ac12437cf..05d5cc584 100644 Binary files a/launcher/resources/multimc/24x24/noaccount.png and b/launcher/resources/multimc/24x24/noaccount.png differ diff --git a/launcher/resources/multimc/24x24/patreon.png b/launcher/resources/multimc/24x24/patreon.png index add806686..2e1cc0548 100644 Binary files a/launcher/resources/multimc/24x24/patreon.png and b/launcher/resources/multimc/24x24/patreon.png differ diff --git a/launcher/resources/multimc/24x24/resourcepacks.png b/launcher/resources/multimc/24x24/resourcepacks.png index 68359d395..b434fb124 100644 Binary files a/launcher/resources/multimc/24x24/resourcepacks.png and b/launcher/resources/multimc/24x24/resourcepacks.png differ diff --git a/launcher/resources/multimc/24x24/star.png b/launcher/resources/multimc/24x24/star.png index 7f16618a6..8527f5092 100644 Binary files a/launcher/resources/multimc/24x24/star.png and b/launcher/resources/multimc/24x24/star.png differ diff --git a/launcher/resources/multimc/24x24/status-bad.png b/launcher/resources/multimc/24x24/status-bad.png index d1547a474..eae695286 100644 Binary files a/launcher/resources/multimc/24x24/status-bad.png and b/launcher/resources/multimc/24x24/status-bad.png differ diff --git a/launcher/resources/multimc/24x24/status-good.png b/launcher/resources/multimc/24x24/status-good.png index 3545bc4c5..e315beaf3 100644 Binary files a/launcher/resources/multimc/24x24/status-good.png and b/launcher/resources/multimc/24x24/status-good.png differ diff --git a/launcher/resources/multimc/24x24/status-running.png b/launcher/resources/multimc/24x24/status-running.png index ecd64451f..9c6059462 100644 Binary files a/launcher/resources/multimc/24x24/status-running.png and b/launcher/resources/multimc/24x24/status-running.png differ diff --git a/launcher/resources/multimc/24x24/status-yellow.png b/launcher/resources/multimc/24x24/status-yellow.png index dd5fde67b..118efd890 100644 Binary files a/launcher/resources/multimc/24x24/status-yellow.png and b/launcher/resources/multimc/24x24/status-yellow.png differ diff --git a/launcher/resources/multimc/256x256/minecraft.png b/launcher/resources/multimc/256x256/minecraft.png index 77e3f03e2..0b24f5501 100644 Binary files a/launcher/resources/multimc/256x256/minecraft.png and b/launcher/resources/multimc/256x256/minecraft.png differ diff --git a/launcher/resources/multimc/32x32/about.png b/launcher/resources/multimc/32x32/about.png index 5174c4f19..261d2a44c 100644 Binary files a/launcher/resources/multimc/32x32/about.png and b/launcher/resources/multimc/32x32/about.png differ diff --git a/launcher/resources/multimc/32x32/bug.png b/launcher/resources/multimc/32x32/bug.png index ada466530..3d97be84a 100644 Binary files a/launcher/resources/multimc/32x32/bug.png and b/launcher/resources/multimc/32x32/bug.png differ diff --git a/launcher/resources/multimc/32x32/cat.png b/launcher/resources/multimc/32x32/cat.png index 78ff98e9e..b9b21e663 100644 Binary files a/launcher/resources/multimc/32x32/cat.png and b/launcher/resources/multimc/32x32/cat.png differ diff --git a/launcher/resources/multimc/32x32/centralmods.png b/launcher/resources/multimc/32x32/centralmods.png index cd2b8208e..7225ba08c 100644 Binary files a/launcher/resources/multimc/32x32/centralmods.png and b/launcher/resources/multimc/32x32/centralmods.png differ diff --git a/launcher/resources/multimc/32x32/checkupdate.png b/launcher/resources/multimc/32x32/checkupdate.png index 754005f97..c60f965b2 100644 Binary files a/launcher/resources/multimc/32x32/checkupdate.png and b/launcher/resources/multimc/32x32/checkupdate.png differ diff --git a/launcher/resources/multimc/32x32/copy.png b/launcher/resources/multimc/32x32/copy.png index c137b0f11..ce662604e 100644 Binary files a/launcher/resources/multimc/32x32/copy.png and b/launcher/resources/multimc/32x32/copy.png differ diff --git a/launcher/resources/multimc/32x32/coremods.png b/launcher/resources/multimc/32x32/coremods.png index 770d695eb..9718ec671 100644 Binary files a/launcher/resources/multimc/32x32/coremods.png and b/launcher/resources/multimc/32x32/coremods.png differ diff --git a/launcher/resources/multimc/32x32/help.png b/launcher/resources/multimc/32x32/help.png index b38542784..6e4cdbff6 100644 Binary files a/launcher/resources/multimc/32x32/help.png and b/launcher/resources/multimc/32x32/help.png differ diff --git a/launcher/resources/multimc/32x32/instance-settings.png b/launcher/resources/multimc/32x32/instance-settings.png index a9c0817c9..4be48c1d5 100644 Binary files a/launcher/resources/multimc/32x32/instance-settings.png and b/launcher/resources/multimc/32x32/instance-settings.png differ diff --git a/launcher/resources/multimc/32x32/instances/brick_legacy.png b/launcher/resources/multimc/32x32/instances/brick_legacy.png index c324fda06..7d35f4da5 100644 Binary files a/launcher/resources/multimc/32x32/instances/brick_legacy.png and b/launcher/resources/multimc/32x32/instances/brick_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/chicken_legacy.png b/launcher/resources/multimc/32x32/instances/chicken_legacy.png index f870467a6..7991410e1 100644 Binary files a/launcher/resources/multimc/32x32/instances/chicken_legacy.png and b/launcher/resources/multimc/32x32/instances/chicken_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/creeper_legacy.png b/launcher/resources/multimc/32x32/instances/creeper_legacy.png index a67ecfc35..571d2de19 100644 Binary files a/launcher/resources/multimc/32x32/instances/creeper_legacy.png and b/launcher/resources/multimc/32x32/instances/creeper_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/diamond_legacy.png b/launcher/resources/multimc/32x32/instances/diamond_legacy.png index 1eb264697..3ad9c002f 100644 Binary files a/launcher/resources/multimc/32x32/instances/diamond_legacy.png and b/launcher/resources/multimc/32x32/instances/diamond_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/dirt_legacy.png b/launcher/resources/multimc/32x32/instances/dirt_legacy.png index 9e19eb8fa..719a45ed5 100644 Binary files a/launcher/resources/multimc/32x32/instances/dirt_legacy.png and b/launcher/resources/multimc/32x32/instances/dirt_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png b/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png index a818eb8e6..e0262f659 100644 Binary files a/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png and b/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/ftb_glow.png b/launcher/resources/multimc/32x32/instances/ftb_glow.png index c4e6fd5d3..7437b27cc 100644 Binary files a/launcher/resources/multimc/32x32/instances/ftb_glow.png and b/launcher/resources/multimc/32x32/instances/ftb_glow.png differ diff --git a/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png b/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png index 20df71710..a70109bbb 100644 Binary files a/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png and b/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/gear_legacy.png b/launcher/resources/multimc/32x32/instances/gear_legacy.png index da9ba2f9d..61dc9f500 100644 Binary files a/launcher/resources/multimc/32x32/instances/gear_legacy.png and b/launcher/resources/multimc/32x32/instances/gear_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/gold_legacy.png b/launcher/resources/multimc/32x32/instances/gold_legacy.png index 593410fac..99d91795c 100644 Binary files a/launcher/resources/multimc/32x32/instances/gold_legacy.png and b/launcher/resources/multimc/32x32/instances/gold_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/grass_legacy.png b/launcher/resources/multimc/32x32/instances/grass_legacy.png index f1694547a..400f21067 100644 Binary files a/launcher/resources/multimc/32x32/instances/grass_legacy.png and b/launcher/resources/multimc/32x32/instances/grass_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/herobrine_legacy.png b/launcher/resources/multimc/32x32/instances/herobrine_legacy.png index e5460da31..8ed872a6f 100644 Binary files a/launcher/resources/multimc/32x32/instances/herobrine_legacy.png and b/launcher/resources/multimc/32x32/instances/herobrine_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/infinity_legacy.png b/launcher/resources/multimc/32x32/instances/infinity_legacy.png index bd94a3dc2..62291c782 100644 Binary files a/launcher/resources/multimc/32x32/instances/infinity_legacy.png and b/launcher/resources/multimc/32x32/instances/infinity_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/iron_legacy.png b/launcher/resources/multimc/32x32/instances/iron_legacy.png index 3e811bd63..d05d7c01e 100644 Binary files a/launcher/resources/multimc/32x32/instances/iron_legacy.png and b/launcher/resources/multimc/32x32/instances/iron_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/magitech_legacy.png b/launcher/resources/multimc/32x32/instances/magitech_legacy.png index 6fd8ff604..bd630da8f 100644 Binary files a/launcher/resources/multimc/32x32/instances/magitech_legacy.png and b/launcher/resources/multimc/32x32/instances/magitech_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/meat_legacy.png b/launcher/resources/multimc/32x32/instances/meat_legacy.png index 6694859db..422c88eeb 100644 Binary files a/launcher/resources/multimc/32x32/instances/meat_legacy.png and b/launcher/resources/multimc/32x32/instances/meat_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/netherstar_legacy.png b/launcher/resources/multimc/32x32/instances/netherstar_legacy.png index 43cb51139..6f5c6f22b 100644 Binary files a/launcher/resources/multimc/32x32/instances/netherstar_legacy.png and b/launcher/resources/multimc/32x32/instances/netherstar_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/planks_legacy.png b/launcher/resources/multimc/32x32/instances/planks_legacy.png index a94b75029..0ff6d19b0 100644 Binary files a/launcher/resources/multimc/32x32/instances/planks_legacy.png and b/launcher/resources/multimc/32x32/instances/planks_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/skeleton_legacy.png b/launcher/resources/multimc/32x32/instances/skeleton_legacy.png index 0c8d3505a..2327a036a 100644 Binary files a/launcher/resources/multimc/32x32/instances/skeleton_legacy.png and b/launcher/resources/multimc/32x32/instances/skeleton_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png b/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png index b78c4ae09..258c9b34d 100644 Binary files a/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png and b/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/steve_legacy.png b/launcher/resources/multimc/32x32/instances/steve_legacy.png index 07c6acdee..3467335f0 100644 Binary files a/launcher/resources/multimc/32x32/instances/steve_legacy.png and b/launcher/resources/multimc/32x32/instances/steve_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/stone_legacy.png b/launcher/resources/multimc/32x32/instances/stone_legacy.png index 1b6ef7a43..7a4d88cf0 100644 Binary files a/launcher/resources/multimc/32x32/instances/stone_legacy.png and b/launcher/resources/multimc/32x32/instances/stone_legacy.png differ diff --git a/launcher/resources/multimc/32x32/instances/tnt_legacy.png b/launcher/resources/multimc/32x32/instances/tnt_legacy.png index e40d404d8..7ab83644f 100644 Binary files a/launcher/resources/multimc/32x32/instances/tnt_legacy.png and b/launcher/resources/multimc/32x32/instances/tnt_legacy.png differ diff --git a/launcher/resources/multimc/32x32/jarmods.png b/launcher/resources/multimc/32x32/jarmods.png index 5cda173a9..848be629f 100644 Binary files a/launcher/resources/multimc/32x32/jarmods.png and b/launcher/resources/multimc/32x32/jarmods.png differ diff --git a/launcher/resources/multimc/32x32/loadermods.png b/launcher/resources/multimc/32x32/loadermods.png index c4ca12e26..73d70a30a 100644 Binary files a/launcher/resources/multimc/32x32/loadermods.png and b/launcher/resources/multimc/32x32/loadermods.png differ diff --git a/launcher/resources/multimc/32x32/log.png b/launcher/resources/multimc/32x32/log.png index d620da122..6c2290f77 100644 Binary files a/launcher/resources/multimc/32x32/log.png and b/launcher/resources/multimc/32x32/log.png differ diff --git a/launcher/resources/multimc/32x32/minecraft.png b/launcher/resources/multimc/32x32/minecraft.png index 816bec98f..6b3642692 100644 Binary files a/launcher/resources/multimc/32x32/minecraft.png and b/launcher/resources/multimc/32x32/minecraft.png differ diff --git a/launcher/resources/multimc/32x32/new.png b/launcher/resources/multimc/32x32/new.png index a3555ba49..8fc4be64a 100644 Binary files a/launcher/resources/multimc/32x32/new.png and b/launcher/resources/multimc/32x32/new.png differ diff --git a/launcher/resources/multimc/32x32/news.png b/launcher/resources/multimc/32x32/news.png index c579fd44d..240803124 100644 Binary files a/launcher/resources/multimc/32x32/news.png and b/launcher/resources/multimc/32x32/news.png differ diff --git a/launcher/resources/multimc/32x32/noaccount.png b/launcher/resources/multimc/32x32/noaccount.png index a73afc946..98ca7130e 100644 Binary files a/launcher/resources/multimc/32x32/noaccount.png and b/launcher/resources/multimc/32x32/noaccount.png differ diff --git a/launcher/resources/multimc/32x32/patreon.png b/launcher/resources/multimc/32x32/patreon.png index 70085aa1c..440195d2e 100644 Binary files a/launcher/resources/multimc/32x32/patreon.png and b/launcher/resources/multimc/32x32/patreon.png differ diff --git a/launcher/resources/multimc/32x32/refresh.png b/launcher/resources/multimc/32x32/refresh.png index afa2a9d77..e67c5fe51 100644 Binary files a/launcher/resources/multimc/32x32/refresh.png and b/launcher/resources/multimc/32x32/refresh.png differ diff --git a/launcher/resources/multimc/32x32/resourcepacks.png b/launcher/resources/multimc/32x32/resourcepacks.png index c14759ef7..8af7fe31f 100644 Binary files a/launcher/resources/multimc/32x32/resourcepacks.png and b/launcher/resources/multimc/32x32/resourcepacks.png differ diff --git a/launcher/resources/multimc/32x32/screenshots.png b/launcher/resources/multimc/32x32/screenshots.png index 4fcd62246..95c8c7e93 100644 Binary files a/launcher/resources/multimc/32x32/screenshots.png and b/launcher/resources/multimc/32x32/screenshots.png differ diff --git a/launcher/resources/multimc/32x32/settings.png b/launcher/resources/multimc/32x32/settings.png index a9c0817c9..4be48c1d5 100644 Binary files a/launcher/resources/multimc/32x32/settings.png and b/launcher/resources/multimc/32x32/settings.png differ diff --git a/launcher/resources/multimc/32x32/star.png b/launcher/resources/multimc/32x32/star.png index b271f0d1b..c797ab34c 100644 Binary files a/launcher/resources/multimc/32x32/star.png and b/launcher/resources/multimc/32x32/star.png differ diff --git a/launcher/resources/multimc/32x32/status-bad.png b/launcher/resources/multimc/32x32/status-bad.png index 8c2c9d4f7..77ac8fe01 100644 Binary files a/launcher/resources/multimc/32x32/status-bad.png and b/launcher/resources/multimc/32x32/status-bad.png differ diff --git a/launcher/resources/multimc/32x32/status-good.png b/launcher/resources/multimc/32x32/status-good.png index 1a805e68b..b8f7095ad 100644 Binary files a/launcher/resources/multimc/32x32/status-good.png and b/launcher/resources/multimc/32x32/status-good.png differ diff --git a/launcher/resources/multimc/32x32/status-running.png b/launcher/resources/multimc/32x32/status-running.png index f561f01ad..8ff17a046 100644 Binary files a/launcher/resources/multimc/32x32/status-running.png and b/launcher/resources/multimc/32x32/status-running.png differ diff --git a/launcher/resources/multimc/32x32/status-yellow.png b/launcher/resources/multimc/32x32/status-yellow.png index 42c685520..36270afb9 100644 Binary files a/launcher/resources/multimc/32x32/status-yellow.png and b/launcher/resources/multimc/32x32/status-yellow.png differ diff --git a/launcher/resources/multimc/32x32/viewfolder.png b/launcher/resources/multimc/32x32/viewfolder.png index 74ab8fa63..32d7b4bae 100644 Binary files a/launcher/resources/multimc/32x32/viewfolder.png and b/launcher/resources/multimc/32x32/viewfolder.png differ diff --git a/launcher/resources/multimc/32x32/worlds.png b/launcher/resources/multimc/32x32/worlds.png index c986596c9..dce4d96b5 100644 Binary files a/launcher/resources/multimc/32x32/worlds.png and b/launcher/resources/multimc/32x32/worlds.png differ diff --git a/launcher/resources/multimc/48x48/about.png b/launcher/resources/multimc/48x48/about.png index b4ac71b8e..f6d4d11cb 100644 Binary files a/launcher/resources/multimc/48x48/about.png and b/launcher/resources/multimc/48x48/about.png differ diff --git a/launcher/resources/multimc/48x48/bug.png b/launcher/resources/multimc/48x48/bug.png index 298f9397c..8de0b0755 100644 Binary files a/launcher/resources/multimc/48x48/bug.png and b/launcher/resources/multimc/48x48/bug.png differ diff --git a/launcher/resources/multimc/48x48/cat.png b/launcher/resources/multimc/48x48/cat.png index 25912a3c0..f84221d7a 100644 Binary files a/launcher/resources/multimc/48x48/cat.png and b/launcher/resources/multimc/48x48/cat.png differ diff --git a/launcher/resources/multimc/48x48/centralmods.png b/launcher/resources/multimc/48x48/centralmods.png index d927e39b4..2425a7c74 100644 Binary files a/launcher/resources/multimc/48x48/centralmods.png and b/launcher/resources/multimc/48x48/centralmods.png differ diff --git a/launcher/resources/multimc/48x48/checkupdate.png b/launcher/resources/multimc/48x48/checkupdate.png index 2e2c7d6be..b181736a8 100644 Binary files a/launcher/resources/multimc/48x48/checkupdate.png and b/launcher/resources/multimc/48x48/checkupdate.png differ diff --git a/launcher/resources/multimc/48x48/copy.png b/launcher/resources/multimc/48x48/copy.png index ea40e34b7..4dc04b080 100644 Binary files a/launcher/resources/multimc/48x48/copy.png and b/launcher/resources/multimc/48x48/copy.png differ diff --git a/launcher/resources/multimc/48x48/help.png b/launcher/resources/multimc/48x48/help.png index 82d828fab..f57c6c896 100644 Binary files a/launcher/resources/multimc/48x48/help.png and b/launcher/resources/multimc/48x48/help.png differ diff --git a/launcher/resources/multimc/48x48/instance-settings.png b/launcher/resources/multimc/48x48/instance-settings.png index 6674eb236..ec298cd62 100644 Binary files a/launcher/resources/multimc/48x48/instance-settings.png and b/launcher/resources/multimc/48x48/instance-settings.png differ diff --git a/launcher/resources/multimc/48x48/log.png b/launcher/resources/multimc/48x48/log.png index 45f60e6b4..dc3eb4e27 100644 Binary files a/launcher/resources/multimc/48x48/log.png and b/launcher/resources/multimc/48x48/log.png differ diff --git a/launcher/resources/multimc/48x48/minecraft.png b/launcher/resources/multimc/48x48/minecraft.png index 38fc9f6cc..4fe522ffb 100644 Binary files a/launcher/resources/multimc/48x48/minecraft.png and b/launcher/resources/multimc/48x48/minecraft.png differ diff --git a/launcher/resources/multimc/48x48/new.png b/launcher/resources/multimc/48x48/new.png index a81753b31..a81ccf1b2 100644 Binary files a/launcher/resources/multimc/48x48/new.png and b/launcher/resources/multimc/48x48/new.png differ diff --git a/launcher/resources/multimc/48x48/news.png b/launcher/resources/multimc/48x48/news.png index 0f82d8577..d2f5d178a 100644 Binary files a/launcher/resources/multimc/48x48/news.png and b/launcher/resources/multimc/48x48/news.png differ diff --git a/launcher/resources/multimc/48x48/noaccount.png b/launcher/resources/multimc/48x48/noaccount.png index 4703796c7..c13e4d6d5 100644 Binary files a/launcher/resources/multimc/48x48/noaccount.png and b/launcher/resources/multimc/48x48/noaccount.png differ diff --git a/launcher/resources/multimc/48x48/patreon.png b/launcher/resources/multimc/48x48/patreon.png index 7aec4d7d3..7e8f25367 100644 Binary files a/launcher/resources/multimc/48x48/patreon.png and b/launcher/resources/multimc/48x48/patreon.png differ diff --git a/launcher/resources/multimc/48x48/refresh.png b/launcher/resources/multimc/48x48/refresh.png index 0b08b2388..87e113583 100644 Binary files a/launcher/resources/multimc/48x48/refresh.png and b/launcher/resources/multimc/48x48/refresh.png differ diff --git a/launcher/resources/multimc/48x48/screenshots.png b/launcher/resources/multimc/48x48/screenshots.png index 03c0059fa..694b96cd9 100644 Binary files a/launcher/resources/multimc/48x48/screenshots.png and b/launcher/resources/multimc/48x48/screenshots.png differ diff --git a/launcher/resources/multimc/48x48/settings.png b/launcher/resources/multimc/48x48/settings.png index 6674eb236..ec298cd62 100644 Binary files a/launcher/resources/multimc/48x48/settings.png and b/launcher/resources/multimc/48x48/settings.png differ diff --git a/launcher/resources/multimc/48x48/star.png b/launcher/resources/multimc/48x48/star.png index d9468e7e3..c5253c334 100644 Binary files a/launcher/resources/multimc/48x48/star.png and b/launcher/resources/multimc/48x48/star.png differ diff --git a/launcher/resources/multimc/48x48/status-bad.png b/launcher/resources/multimc/48x48/status-bad.png index 41c9cf227..083506d28 100644 Binary files a/launcher/resources/multimc/48x48/status-bad.png and b/launcher/resources/multimc/48x48/status-bad.png differ diff --git a/launcher/resources/multimc/48x48/status-good.png b/launcher/resources/multimc/48x48/status-good.png index df7cb59b6..0c3377ad7 100644 Binary files a/launcher/resources/multimc/48x48/status-good.png and b/launcher/resources/multimc/48x48/status-good.png differ diff --git a/launcher/resources/multimc/48x48/status-running.png b/launcher/resources/multimc/48x48/status-running.png index b8c0bf7cd..94598c927 100644 Binary files a/launcher/resources/multimc/48x48/status-running.png and b/launcher/resources/multimc/48x48/status-running.png differ diff --git a/launcher/resources/multimc/48x48/status-yellow.png b/launcher/resources/multimc/48x48/status-yellow.png index 4f3b11d69..bb76fcd69 100644 Binary files a/launcher/resources/multimc/48x48/status-yellow.png and b/launcher/resources/multimc/48x48/status-yellow.png differ diff --git a/launcher/resources/multimc/48x48/viewfolder.png b/launcher/resources/multimc/48x48/viewfolder.png index 0492a736c..2245ba30a 100644 Binary files a/launcher/resources/multimc/48x48/viewfolder.png and b/launcher/resources/multimc/48x48/viewfolder.png differ diff --git a/launcher/resources/multimc/48x48/worlds.png b/launcher/resources/multimc/48x48/worlds.png index 4fc337511..eb44150a3 100644 Binary files a/launcher/resources/multimc/48x48/worlds.png and b/launcher/resources/multimc/48x48/worlds.png differ diff --git a/launcher/resources/multimc/50x50/instances/enderman_legacy.png b/launcher/resources/multimc/50x50/instances/enderman_legacy.png index 9f3a72b3a..36c791eb0 100644 Binary files a/launcher/resources/multimc/50x50/instances/enderman_legacy.png and b/launcher/resources/multimc/50x50/instances/enderman_legacy.png differ diff --git a/launcher/resources/multimc/64x64/about.png b/launcher/resources/multimc/64x64/about.png index b83e92690..b9be9abef 100644 Binary files a/launcher/resources/multimc/64x64/about.png and b/launcher/resources/multimc/64x64/about.png differ diff --git a/launcher/resources/multimc/64x64/bug.png b/launcher/resources/multimc/64x64/bug.png index 156b03159..6c9ac6af2 100644 Binary files a/launcher/resources/multimc/64x64/bug.png and b/launcher/resources/multimc/64x64/bug.png differ diff --git a/launcher/resources/multimc/64x64/cat.png b/launcher/resources/multimc/64x64/cat.png index 2cc21f808..65681e6b8 100644 Binary files a/launcher/resources/multimc/64x64/cat.png and b/launcher/resources/multimc/64x64/cat.png differ diff --git a/launcher/resources/multimc/64x64/centralmods.png b/launcher/resources/multimc/64x64/centralmods.png index 8831f437c..d30735601 100644 Binary files a/launcher/resources/multimc/64x64/centralmods.png and b/launcher/resources/multimc/64x64/centralmods.png differ diff --git a/launcher/resources/multimc/64x64/checkupdate.png b/launcher/resources/multimc/64x64/checkupdate.png index dd1e29ac6..a4002a61e 100644 Binary files a/launcher/resources/multimc/64x64/checkupdate.png and b/launcher/resources/multimc/64x64/checkupdate.png differ diff --git a/launcher/resources/multimc/64x64/copy.png b/launcher/resources/multimc/64x64/copy.png index d12cf9c8a..69fa1c3fb 100644 Binary files a/launcher/resources/multimc/64x64/copy.png and b/launcher/resources/multimc/64x64/copy.png differ diff --git a/launcher/resources/multimc/64x64/coremods.png b/launcher/resources/multimc/64x64/coremods.png index 668be3341..b1b1f8237 100644 Binary files a/launcher/resources/multimc/64x64/coremods.png and b/launcher/resources/multimc/64x64/coremods.png differ diff --git a/launcher/resources/multimc/64x64/help.png b/launcher/resources/multimc/64x64/help.png index 0f3948c2c..e419f8600 100644 Binary files a/launcher/resources/multimc/64x64/help.png and b/launcher/resources/multimc/64x64/help.png differ diff --git a/launcher/resources/multimc/64x64/instance-settings.png b/launcher/resources/multimc/64x64/instance-settings.png index e3ff58faf..9df7fe9bc 100644 Binary files a/launcher/resources/multimc/64x64/instance-settings.png and b/launcher/resources/multimc/64x64/instance-settings.png differ diff --git a/launcher/resources/multimc/64x64/jarmods.png b/launcher/resources/multimc/64x64/jarmods.png index 55d1a42a0..5abd5ecc5 100644 Binary files a/launcher/resources/multimc/64x64/jarmods.png and b/launcher/resources/multimc/64x64/jarmods.png differ diff --git a/launcher/resources/multimc/64x64/loadermods.png b/launcher/resources/multimc/64x64/loadermods.png index 24618fd0b..485aa843a 100644 Binary files a/launcher/resources/multimc/64x64/loadermods.png and b/launcher/resources/multimc/64x64/loadermods.png differ diff --git a/launcher/resources/multimc/64x64/log.png b/launcher/resources/multimc/64x64/log.png index 0f531cdfc..decee34bd 100644 Binary files a/launcher/resources/multimc/64x64/log.png and b/launcher/resources/multimc/64x64/log.png differ diff --git a/launcher/resources/multimc/64x64/new.png b/launcher/resources/multimc/64x64/new.png index c3c6796c4..289a6ad0b 100644 Binary files a/launcher/resources/multimc/64x64/new.png and b/launcher/resources/multimc/64x64/new.png differ diff --git a/launcher/resources/multimc/64x64/news.png b/launcher/resources/multimc/64x64/news.png index e306eed37..a1c28fdd6 100644 Binary files a/launcher/resources/multimc/64x64/news.png and b/launcher/resources/multimc/64x64/news.png differ diff --git a/launcher/resources/multimc/64x64/patreon.png b/launcher/resources/multimc/64x64/patreon.png index ef5d690eb..5c2d88814 100644 Binary files a/launcher/resources/multimc/64x64/patreon.png and b/launcher/resources/multimc/64x64/patreon.png differ diff --git a/launcher/resources/multimc/64x64/refresh.png b/launcher/resources/multimc/64x64/refresh.png index 8373d8198..737bd0581 100644 Binary files a/launcher/resources/multimc/64x64/refresh.png and b/launcher/resources/multimc/64x64/refresh.png differ diff --git a/launcher/resources/multimc/64x64/resourcepacks.png b/launcher/resources/multimc/64x64/resourcepacks.png index fb874e7d3..703fde6b5 100644 Binary files a/launcher/resources/multimc/64x64/resourcepacks.png and b/launcher/resources/multimc/64x64/resourcepacks.png differ diff --git a/launcher/resources/multimc/64x64/screenshots.png b/launcher/resources/multimc/64x64/screenshots.png index af18e39ca..a57bf2772 100644 Binary files a/launcher/resources/multimc/64x64/screenshots.png and b/launcher/resources/multimc/64x64/screenshots.png differ diff --git a/launcher/resources/multimc/64x64/settings.png b/launcher/resources/multimc/64x64/settings.png index e3ff58faf..9df7fe9bc 100644 Binary files a/launcher/resources/multimc/64x64/settings.png and b/launcher/resources/multimc/64x64/settings.png differ diff --git a/launcher/resources/multimc/64x64/star.png b/launcher/resources/multimc/64x64/star.png index 4ed5d978f..24e9d75c7 100644 Binary files a/launcher/resources/multimc/64x64/star.png and b/launcher/resources/multimc/64x64/star.png differ diff --git a/launcher/resources/multimc/64x64/status-bad.png b/launcher/resources/multimc/64x64/status-bad.png index 64060ba09..669d3159d 100644 Binary files a/launcher/resources/multimc/64x64/status-bad.png and b/launcher/resources/multimc/64x64/status-bad.png differ diff --git a/launcher/resources/multimc/64x64/status-good.png b/launcher/resources/multimc/64x64/status-good.png index e862ddcdf..4d256cc04 100644 Binary files a/launcher/resources/multimc/64x64/status-good.png and b/launcher/resources/multimc/64x64/status-good.png differ diff --git a/launcher/resources/multimc/64x64/status-running.png b/launcher/resources/multimc/64x64/status-running.png index 38afda0f9..64d6d0a8d 100644 Binary files a/launcher/resources/multimc/64x64/status-running.png and b/launcher/resources/multimc/64x64/status-running.png differ diff --git a/launcher/resources/multimc/64x64/status-yellow.png b/launcher/resources/multimc/64x64/status-yellow.png index 3d54d320c..98013151b 100644 Binary files a/launcher/resources/multimc/64x64/status-yellow.png and b/launcher/resources/multimc/64x64/status-yellow.png differ diff --git a/launcher/resources/multimc/64x64/viewfolder.png b/launcher/resources/multimc/64x64/viewfolder.png index 7d531f9cc..d16cacc4d 100644 Binary files a/launcher/resources/multimc/64x64/viewfolder.png and b/launcher/resources/multimc/64x64/viewfolder.png differ diff --git a/launcher/resources/multimc/64x64/worlds.png b/launcher/resources/multimc/64x64/worlds.png index 1d40f1df7..25aa1d685 100644 Binary files a/launcher/resources/multimc/64x64/worlds.png and b/launcher/resources/multimc/64x64/worlds.png differ diff --git a/launcher/resources/multimc/8x8/noaccount.png b/launcher/resources/multimc/8x8/noaccount.png index 466e4c076..645ea1bed 100644 Binary files a/launcher/resources/multimc/8x8/noaccount.png and b/launcher/resources/multimc/8x8/noaccount.png differ diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 25edd09e0..2937cc34b 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -74,7 +74,7 @@ scalable/screenshots.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg 16x16/cat.png @@ -247,12 +247,11 @@ scalable/matrix.svg - + scalable/discord.svg - scalable/instances/flame.svg scalable/instances/chicken.svg scalable/instances/creeper.svg scalable/instances/enderpearl.svg @@ -279,7 +278,7 @@ scalable/instances/fox.svg scalable/instances/bee.svg - + 32x32/instances/chicken_legacy.png 128x128/instances/chicken_legacy.png @@ -347,6 +346,7 @@ scalable/export.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg scalable/instances/quiltmc.svg scalable/instances/fabricmc.svg diff --git a/launcher/resources/multimc/scalable/appearance.svg b/launcher/resources/multimc/scalable/appearance.svg new file mode 100644 index 000000000..429670c36 --- /dev/null +++ b/launcher/resources/multimc/scalable/appearance.svg @@ -0,0 +1,2440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + diff --git a/launcher/resources/multimc/scalable/atlauncher-placeholder.png b/launcher/resources/multimc/scalable/atlauncher-placeholder.png index f4314c434..8b6dedad5 100644 Binary files a/launcher/resources/multimc/scalable/atlauncher-placeholder.png and b/launcher/resources/multimc/scalable/atlauncher-placeholder.png differ diff --git a/launcher/resources/multimc/scalable/environment-variables.svg b/launcher/resources/multimc/scalable/datapacks.svg similarity index 100% rename from launcher/resources/multimc/scalable/environment-variables.svg rename to launcher/resources/multimc/scalable/datapacks.svg diff --git a/launcher/resources/multimc/scalable/launcher.svg b/launcher/resources/multimc/scalable/launcher.svg index d4386d47b..aeee84338 100644 --- a/launcher/resources/multimc/scalable/launcher.svg +++ b/launcher/resources/multimc/scalable/launcher.svg @@ -1,20 +1,57 @@ - - - - - - - - - - \ No newline at end of file + + + + Prism Launcher Logo + + + + + + + + + + + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 717d3972e..1e6b5d3cc 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_blue/scalable/appearance.svg b/launcher/resources/pe_blue/scalable/appearance.svg new file mode 100644 index 000000000..9323ec02d --- /dev/null +++ b/launcher/resources/pe_blue/scalable/appearance.svg @@ -0,0 +1,65 @@ + + + + diff --git a/launcher/resources/pe_blue/scalable/environment-variables.svg b/launcher/resources/pe_blue/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_blue/scalable/environment-variables.svg rename to launcher/resources/pe_blue/scalable/datapacks.svg diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 023c81e74..71b38024a 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_colored/scalable/appearance.svg b/launcher/resources/pe_colored/scalable/appearance.svg new file mode 100644 index 000000000..88c1eaf26 --- /dev/null +++ b/launcher/resources/pe_colored/scalable/appearance.svg @@ -0,0 +1,71 @@ + + + + diff --git a/launcher/resources/pe_colored/scalable/environment-variables.svg b/launcher/resources/pe_colored/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_colored/scalable/environment-variables.svg rename to launcher/resources/pe_colored/scalable/datapacks.svg diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index c97fb469c..9ccfece1e 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_dark/scalable/appearance.svg b/launcher/resources/pe_dark/scalable/appearance.svg new file mode 100644 index 000000000..24b7283e4 --- /dev/null +++ b/launcher/resources/pe_dark/scalable/appearance.svg @@ -0,0 +1,65 @@ + + + + diff --git a/launcher/resources/pe_dark/scalable/environment-variables.svg b/launcher/resources/pe_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_dark/scalable/environment-variables.svg rename to launcher/resources/pe_dark/scalable/datapacks.svg diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index b590dd2c6..a6d49b803 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_light/scalable/appearance.svg b/launcher/resources/pe_light/scalable/appearance.svg new file mode 100644 index 000000000..61b2f3422 --- /dev/null +++ b/launcher/resources/pe_light/scalable/appearance.svg @@ -0,0 +1,66 @@ + + + + diff --git a/launcher/resources/pe_light/scalable/environment-variables.svg b/launcher/resources/pe_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_light/scalable/environment-variables.svg rename to launcher/resources/pe_light/scalable/datapacks.svg diff --git a/launcher/resources/shaders/fshader.glsl b/launcher/resources/shaders/fshader.glsl new file mode 100644 index 000000000..d6a93db5d --- /dev/null +++ b/launcher/resources/shaders/fshader.glsl @@ -0,0 +1,20 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/fshader.glsl +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +void main() +{ + // Set fragment color from texture + vec4 texColor = texture2D(texture, v_texcoord); + if (texColor.a < 0.1) discard; // Optional: Discard fully transparent pixels + gl_FragColor = texColor; +} diff --git a/launcher/resources/shaders/shaders.qrc b/launcher/resources/shaders/shaders.qrc new file mode 100644 index 000000000..005bdbcbe --- /dev/null +++ b/launcher/resources/shaders/shaders.qrc @@ -0,0 +1,7 @@ + + + vshader_skin_model.glsl + vshader_skin_background.glsl + fshader.glsl + + diff --git a/launcher/resources/shaders/vshader_skin_background.glsl b/launcher/resources/shaders/vshader_skin_background.glsl new file mode 100644 index 000000000..9072af6bb --- /dev/null +++ b/launcher/resources/shaders/vshader_skin_background.glsl @@ -0,0 +1,11 @@ + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +void main() +{ + gl_Position = a_position; + v_texcoord = a_texcoord; +} diff --git a/launcher/resources/shaders/vshader_skin_model.glsl b/launcher/resources/shaders/vshader_skin_model.glsl new file mode 100644 index 000000000..ed8e75778 --- /dev/null +++ b/launcher/resources/shaders/vshader_skin_model.glsl @@ -0,0 +1,37 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/vshader.glsl + +// Dylan Schooner - 2025 +// Modification: Implemented final Z-NDC re-inversion to compensate +// for rigid OpenGL 2.0 context forcing glClearDepth(1.0). +// This flips the high-precision Reverse Z output to the standard [0, W] range. + +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; +uniform mat4 model_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +void main() +{ + // Calculate vertex position in screen space + gl_Position = mvp_matrix * model_matrix * a_position; + + // Invert the z component of our Reverse Z matrix back to standard NDC + float near_z = gl_Position.z; + float w_c = gl_Position.w; + gl_Position.z = w_c - near_z; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} diff --git a/launcher/resources/sources/burfcat_hat.png b/launcher/resources/sources/burfcat_hat.png index a378c1fbb..6abf17820 100644 Binary files a/launcher/resources/sources/burfcat_hat.png and b/launcher/resources/sources/burfcat_hat.png differ diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7ee98760a..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" } })); @@ -86,6 +86,7 @@ auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State auto ImgurAlbumCreation::Sink::abort() -> Task::State { m_output.clear(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -95,14 +96,16 @@ auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); + m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); + m_fail_reason = "Failed to create album"; return Task::State::Failed; } m_result->deleteHash = object.value("data").toObject().value("deletehash").toString(); m_result->id = object.value("data").toObject().value("id").toString(); return Task::State::Succeeded; -} \ No newline at end of file +} diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 8b4ef5327..f553bc774 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -90,6 +90,7 @@ auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State auto ImgurUpload::Sink::abort() -> Task::State { m_output.clear(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -99,11 +100,13 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << "Screenshot upload not successful:" << doc.toJson(); + m_fail_reason = "Screenshot was not uploaded successfully"; return Task::State::Failed; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); @@ -117,7 +120,7 @@ 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_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 2c7620e65..75e888938 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -44,13 +44,14 @@ #include #include +#include "Json.h" INIFile::INIFile() {} bool INIFile::saveFile(QString fileName) { if (!contains("ConfigVersion")) - insert("ConfigVersion", "1.2"); + insert("ConfigVersion", "1.3"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); _settings_obj.clear(); @@ -149,6 +150,27 @@ bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) return true; } +QVariant migrateQByteArrayToBase64(QString key, QVariant value) +{ + static const QStringList otherByteArrays = { "MainWindowState", "MainWindowGeometry", "ConsoleWindowState", + "ConsoleWindowGeometry", "PagedGeometry", "NewInstanceGeometry", + "ModDownloadGeometry", "RPDownloadGeometry", "TPDownloadGeometry", + "ShaderDownloadGeometry" }; + if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { + return QString::fromUtf8(value.toByteArray().toBase64()); + } + if (otherByteArrays.contains(key)) { + return QString::fromUtf8(value.toByteArray()); + } + if (key == "linkedInstances") { + return Json::fromStringList(value.toStringList()); + } + if (key == "Env") { + return Json::fromMap(value.toMap()); + } + return value; +} + bool INIFile::loadFile(QString fileName) { QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; @@ -168,22 +190,34 @@ bool INIFile::loadFile(QString fileName) QSettings::SettingsMap map; parseOldFileFormat(file, map); file.close(); - for (auto&& key : map.keys()) - insert(key, map.value(key)); - insert("ConfigVersion", "1.2"); + for (auto&& key : map.keys()) { + auto value = migrateQByteArrayToBase64(key, map.value(key)); + insert(key, value); + } + insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") { for (auto&& key : _settings_obj.allKeys()) { - if (auto valueStr = _settings_obj.value(key).toString(); + auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); + if (auto valueStr = value.toString(); (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) && valueStr.endsWith("\"") && valueStr.startsWith("\"")) { insert(key, unquote(valueStr)); - } else - insert(key, _settings_obj.value(key)); + } else { + insert(key, value); + } + } + insert("ConfigVersion", "1.3"); + } else if (_settings_obj.value("ConfigVersion").toString() == "1.2") { + for (auto&& key : _settings_obj.allKeys()) { + auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); + insert(key, value); } - insert("ConfigVersion", "1.2"); - } else - for (auto&& key : _settings_obj.allKeys()) + insert("ConfigVersion", "1.3"); + } else { + for (auto&& key : _settings_obj.allKeys()) { insert(key, _settings_obj.value(key)); + } + } return true; } diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp index 1e5dce251..dda8326cf 100644 --- a/launcher/settings/SettingsObject.cpp +++ b/launcher/settings/SettingsObject.cpp @@ -19,7 +19,13 @@ #include "settings/OverrideSetting.h" #include "settings/Setting.h" +#include #include +#include + +#ifdef Q_OS_MACOS +#include "macsandbox/SecurityBookmarkFileAccess.h" +#endif SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {} @@ -78,9 +84,17 @@ std::shared_ptr SettingsObject::getSetting(const QString& id) const return m_settings[id]; } -QVariant SettingsObject::get(const QString& id) const +QVariant SettingsObject::get(const QString& id) { auto setting = getSetting(id); + +#ifdef Q_OS_MACOS + // for macOS, use a security scoped bookmark for the paths + if (id.endsWith("Dir")) { + return { getPathFromBookmark(id) }; + } +#endif + return (setting ? setting->get() : QVariant()); } @@ -90,11 +104,106 @@ bool SettingsObject::set(const QString& id, QVariant value) if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return false; - } else { - setting->set(value); + } + +#ifdef Q_OS_MACOS + // for macOS, keep a security scoped bookmark for the paths + if (value.userType() == QMetaType::QString && id.endsWith("Dir")) { + setPathWithBookmark(id, value.toString()); + } +#endif + + setting->set(std::move(value)); + return true; +} + +#ifdef Q_OS_MACOS +QString SettingsObject::getPathFromBookmark(const QString& id) +{ + auto setting = getSetting(id); + if (!setting) { + qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); + return ""; + } + + // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) + if (setting->get() == setting->defValue() || + QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) { + return setting->get().toString(); + } + + auto bookmarkId = id + "Bookmark"; + auto bookmarkSetting = getSetting(bookmarkId); + if (!bookmarkSetting) { + qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id); + return ""; + } + + QByteArray bookmark = bookmarkSetting->get().toByteArray(); + if (bookmark.isEmpty()) { + qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString(); + setPathWithBookmark(id, setting->get().toString()); + return setting->get().toString(); + } + bool stale; + QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale); + if (url.isValid()) { + if (stale) { + setting->set(url.path()); + bookmarkSetting->set(bookmark); + } + + m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale); + // already did a stale check, no need to do it again + + // convert to relative path to current directory if `url` is a descendant of the current directory + QDir currentDir = QDir::current().absolutePath(); + return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path(); + } + + return setting->get().toString(); +} + +bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path) +{ + auto setting = getSetting(id); + if (!setting) { + qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); + return false; + } + + QDir dir(path); + if (!dir.exists()) { + qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id); + return false; + } + QString absolutePath = dir.absolutePath(); + QString bookmarkId = id + "Bookmark"; + std::shared_ptr bookmarkSetting = getSetting(bookmarkId); + // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) + if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) { + bookmarkSetting->reset(); return true; } + QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath); + if (bytes.isEmpty()) { + qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id); + // TODO: show an alert to the user asking them to reselect the directory + return false; + } + auto oldBookmark = bookmarkSetting->get().toByteArray(); + m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark); + if (!bytes.isEmpty() && bookmarkSetting) { + bookmarkSetting->set(bytes); + bool stale; + m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale); + // just created the bookmark, it shouldn't be stale + } + + setting->set(path); + return true; } +#endif void SettingsObject::reset(const QString& id) const { @@ -119,8 +228,13 @@ bool SettingsObject::reload() void SettingsObject::connectSignals(const Setting& setting) { connect(&setting, &Setting::SettingChanged, this, &SettingsObject::changeSetting); - connect(&setting, SIGNAL(SettingChanged(const Setting&, QVariant)), this, SIGNAL(SettingChanged(const Setting&, QVariant))); + connect(&setting, &Setting::SettingChanged, this, &SettingsObject::SettingChanged); connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); - connect(&setting, SIGNAL(settingReset(Setting)), this, SIGNAL(settingReset(const Setting&))); + connect(&setting, &Setting::settingReset, this, &SettingsObject::settingReset); +} + +std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) +{ + return contains(id) ? getSetting(id) : registerSetting(id, defVal); } diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index f133f2f7f..5743bee54 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -23,12 +23,13 @@ #include #include +#ifdef Q_OS_MACOS +#include "macsandbox/SecurityBookmarkFileAccess.h" +#endif + 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. @@ -46,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: @@ -103,13 +104,43 @@ class SettingsObject : public QObject { */ std::shared_ptr getSetting(const QString& id) const; + /*! + * \brief Gets the setting with the given ID. + * \brief if is not registered yet it does that + * \param id The ID of the setting to get. + * \return A pointer to the setting with the given ID. + * Returns null if there is no setting with the given ID. + * \sa operator []() + */ + std::shared_ptr getOrRegisterSetting(const QString& id, QVariant defVal = QVariant()); + /*! * \brief Gets the value of the setting with the given ID. * \param id The ID of the setting to get. * \return The setting's value as a QVariant. * If no setting with the given ID exists, returns an invalid QVariant. */ - QVariant get(const QString& id) const; + QVariant get(const QString& id); + +#ifdef Q_OS_MACOS + /*! + * \brief Get the path to the file or directory represented by the bookmark stored in the associated setting. + * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. + * \return A path to the file or directory represented by the bookmark. + * If a bookmark is not valid or stored, use default logic (directly return the stored path). + * This can attempt to create a bookmark if the path is accessible and the bookmark is not valid. + */ + QString getPathFromBookmark(const QString& id); + /*! + * \brief Set a security-scoped bookmark to the provided path for the associated setting. + * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. + * \param path The new desired path. + * \return A boolean indicating whether a bookmark was successfully set. + * The path needs to be accessible to the launcher before calling this function. For example, + * it could come from a user selection in an open panel. + */ + bool setPathWithBookmark(const QString& id, const QString& path); +#endif /*! * \brief Sets the value of the setting with the given ID. @@ -197,6 +228,9 @@ class SettingsObject : public QObject { private: QMap> m_settings; +#ifdef Q_OS_MACOS + SecurityBookmarkFileAccess m_sandboxedFileAccess; +#endif protected: bool m_suspendSave = false; diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index ad2a14c42..84530ec99 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -118,10 +118,29 @@ void ConcurrentTask::executeNextSubTask() } if (m_queue.isEmpty()) { if (m_doing.isEmpty()) { - if (m_failed.isEmpty()) + if (m_failed.isEmpty()) { emitSucceeded(); - else - emitFailed(tr("One or more subtasks failed")); + } else if (m_failed.count() == 1) { + auto task = m_failed.keys().first(); + auto reason = task->failReason(); + if (reason.isEmpty()) { // clearly a bug somewhere + reason = tr("Task failed"); + } + emitFailed(reason); + } else { + QStringList failReason; + for (auto t : m_failed) { + auto reason = t->failReason(); + if (!reason.isEmpty()) { + failReason << reason; + } + } + if (failReason.isEmpty()) { + emitFailed(tr("Multiple subtasks failed")); + } else { + emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n"))); + } + } } return; } diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index d988623b9..a65613bf2 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -43,6 +43,10 @@ #include "tasks/Task.h" +/*! + * Runs a list of tasks concurrently (according to `max_concurrent` parameter). + * Behaviour is the same as regular Task (e.g. starts using start()) + */ class ConcurrentTask : public Task { Q_OBJECT public: @@ -59,6 +63,7 @@ class ConcurrentTask : public Task { inline auto isMultiStep() const -> bool override { return totalSize() > 1; } auto getStepProgress() const -> TaskStepProgressList override; + //! Adds a task to execute in this ConcurrentTask void addTask(Task::Ptr task); public slots: @@ -83,7 +88,7 @@ class ConcurrentTask : public Task { protected: // NOTE: This is not thread-safe. - [[nodiscard]] unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } + unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } virtual void updateState(); diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 1871c5df8..92b345c8d 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -196,6 +196,8 @@ void Task::logWarning(const QString& line) { qWarning() << line; m_Warnings.append(line); + + emit warningLogged(line); } QStringList Task::warnings() const diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index e712700a1..43e71c8ab 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -79,6 +79,12 @@ Q_DECLARE_METATYPE(TaskStepProgress) using TaskStepProgressList = QList>; +/*! + * Represents a task that has to be done. + * To create a task, you need to subclass this class, implement the executeTask() method and call + * emitSucceeded() or emitFailed() when the task is done. + * the caller needs to call start() to start the task. + */ class Task : public QObject, public QRunnable { Q_OBJECT public: @@ -130,23 +136,28 @@ class Task : public QObject, public QRunnable { signals: void started(); void progress(qint64 current, qint64 total); + //! called when a task has either succeeded, aborted or failed. void finished(); + //! called when a task has succeeded void succeeded(); + //! called when a task has been aborted by calling abort() void aborted(); void failed(QString reason); void status(QString status); void details(QString details); + void warningLogged(const QString& warning); void stepProgress(TaskStepProgress const& task_progress); - /** Emitted when the canAbort() status has changed. - */ + //! Emitted when the canAbort() status has changed. */ void abortStatusChanged(bool can_abort); public slots: // QRunnable's interface void run() override { start(); } + //! used by the task caller to start the task virtual void start(); + //! used by external code to ask the task to abort virtual bool abort() { if (canAbort()) @@ -161,11 +172,16 @@ class Task : public QObject, public QRunnable { } protected: + //! The task subclass must implement this method. This method is called to start to run the task. + //! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead. virtual void executeTask() = 0; protected slots: + //! The Task subclass must call this method when the task has succeeded virtual void emitSucceeded(); + //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. virtual void emitAborted(); + //! The Task subclass must call this method when the task has failed virtual void emitFailed(QString reason = ""); virtual void propagateStepProgress(TaskStepProgress const& task_progress); diff --git a/launcher/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 594024a7d..66c63d01a 100644 --- a/launcher/tools/GenericProfiler.cpp +++ b/launcher/tools/GenericProfiler.cpp @@ -24,23 +24,23 @@ 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); } -#include "GenericProfiler.moc" \ No newline at end of file +#include "GenericProfiler.moc" 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 7a532a3d2..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); @@ -57,20 +57,20 @@ void JProfiler::beginProfilingImpl(shared_qobject_ptr process) profiler->setProgram(profilerProgram); connect(profiler, &QProcess::started, this, &JProfiler::profilerStarted); - connect(profiler, QOverload::of(&QProcess::finished), this, &JProfiler::profilerFinished); + connect(profiler, &QProcess::finished, this, &JProfiler::profilerFinished); m_profilerProcess = profiler; 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 4da4e1e54..9155a1832 100644 --- a/launcher/tools/JVisualVM.cpp +++ b/launcher/tools/JVisualVM.cpp @@ -10,21 +10,21 @@ 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() { - emit readyToLaunch(tr("JVisualVM started")); + emit readyToLaunch(tr("VisualVM started")); } void JVisualVM::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) @@ -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()) }; @@ -48,13 +48,13 @@ void JVisualVM::beginProfilingImpl(shared_qobject_ptr process) profiler->setProgram(programPath); connect(profiler, &QProcess::started, this, &JVisualVM::profilerStarted); - connect(profiler, QOverload::of(&QProcess::finished), this, &JVisualVM::profilerFinished); + connect(profiler, &QProcess::finished, this, &JVisualVM::profilerFinished); profiler->start(); 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); } @@ -82,7 +82,7 @@ bool JVisualVMFactory::check(const QString& path, QString* error) } QFileInfo finfo(path); if (!finfo.isExecutable() || !finfo.fileName().contains("visualvm")) { - *error = QObject::tr("Invalid path to JVisualVM"); + *error = QObject::tr("Invalid path to VisualVM"); return false; } return true; diff --git a/launcher/tools/JVisualVM.h b/launcher/tools/JVisualVM.h index 2828119a1..dfb09caf4 100644 --- a/launcher/tools/JVisualVM.h +++ b/launcher/tools/JVisualVM.h @@ -4,9 +4,9 @@ class JVisualVMFactory : public BaseProfilerFactory { public: - QString name() const override { return "JVisualVM"; } - void registerSettings(SettingsObjectPtr settings) override; - BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; + QString name() const override { return "VisualVM"; } + 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 19bd5a062..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; @@ -45,7 +45,7 @@ bool MCEditTool::check(const QString& toolPath, QString& error) QString MCEditTool::getProgramPath() { -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS return path(); #else const QString mceditPath = path(); 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/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index d03469b78..ee2e240ab 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -73,7 +73,7 @@ struct Language { if (key == "ja_KANJI") { result = locale.nativeLanguageName() + u8" (漢字)"; } else if (key == "es_UY") { - result = u8"español de Latinoamérica"; + result = u8"Español de Latinoamérica"; } else if (key == "en_NZ") { result = u8"New Zealand English"; // No idea why qt translates this to just english and not to New Zealand English } else if (key == "en@pirate") { @@ -153,7 +153,7 @@ struct TranslationsModel::Private { QDir m_dir; // initial state is just english - QVector m_languages = { Language(defaultLangCode) }; + QList m_languages = { Language(defaultLangCode) }; QString m_selectedLanguage = defaultLangCode; std::unique_ptr m_qt_translator; @@ -251,8 +251,7 @@ void readIndex(const QString& path, QMap& languages) Language lang(iter.key()); auto langObj = Json::requireObject(iter.value()); - lang.setTranslationStats(Json::ensureInteger(langObj, "translated", 0), Json::ensureInteger(langObj, "untranslated", 0), - Json::ensureInteger(langObj, "fuzzy", 0)); + 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"); @@ -417,9 +416,9 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c return 2; } -QVector::Iterator TranslationsModel::findLanguage(const QString& key) +QList::Iterator TranslationsModel::findLanguage(const QString& key) { - return std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; }); + return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; }); } std::optional TranslationsModel::findLanguageAsOptional(const QString& key) @@ -480,7 +479,7 @@ bool TranslationsModel::selectLanguage(QString key) bool successful = false; // FIXME: this is likely never present. FIX IT. d->m_qt_translator.reset(new QTranslator()); - if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) { qCritical() << "Loading Qt Language File failed."; diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 96a0e9f8b..945e689fc 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -41,7 +41,7 @@ class TranslationsModel : public QAbstractListModel { void setUseSystemLocale(bool useSystemLocale); private: - QVector::Iterator findLanguage(const QString& key); + QList::Iterator findLanguage(const QString& key); std::optional findLanguageAsOptional(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 94dd01169..141153b92 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -38,10 +38,15 @@ #include "GuiUtil.h" #include +#include #include #include #include +#include "FileSystem.h" +#include "logs/AnonymizeLog.h" +#include "net/NetJob.h" +#include "net/NetRequest.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -74,52 +79,52 @@ QString truncateLogForMclogs(const QString& logContent) return logContent; } +std::optional GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget) +{ + return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget); +}; + std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); - auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + auto pasteType = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); + auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); bool shouldTruncate = false; - { - QUrl baseUrl; - if (pasteCustomAPIBaseSetting.isEmpty()) - baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase; - else - baseUrl = pasteCustomAPIBaseSetting; - - if (baseUrl.isValid()) { - auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), - QObject::tr("You are about to upload \"%1\" to %2.\n" - "You should double-check for personal information.\n\n" - "Are you sure?") - .arg(name, baseUrl.host()), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - - if (response != QMessageBox::Yes) + if (baseURL.isEmpty()) + baseURL = PasteUpload::PasteTypes[pasteType].defaultBase; + + if (auto url = QUrl(baseURL); url.isValid()) { + auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), + QObject::tr("You are about to upload \"%1\" to %2.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(name, url.host()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return {}; + + if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { + auto truncateResponse = CustomMessageBox::selectable( + parentWidget, QObject::tr("Confirm Truncation"), + QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" + "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" + "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " + "potentially useful info like crashes at the end.\n\n" + "Proceed with truncation?") + .arg(text.count("\n")) + .arg(MaxMclogsLines) + .arg(InitialMclogsLines) + .arg(FinalMclogsLines), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) + ->exec(); + + if (truncateResponse == QMessageBox::Cancel) { return {}; - - if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { - auto truncateResponse = CustomMessageBox::selectable( - parentWidget, QObject::tr("Confirm Truncation"), - QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" - "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" - "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " - "potentially useful info like crashes at the end.\n\n" - "Proceed with truncation?") - .arg(text.count("\n")) - .arg(MaxMclogsLines) - .arg(InitialMclogsLines) - .arg(FinalMclogsLines), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) - ->exec(); - - if (truncateResponse == QMessageBox::Cancel) { - return {}; - } - shouldTruncate = truncateResponse == QMessageBox::Yes; } + shouldTruncate = truncateResponse == QMessageBox::Yes; } } @@ -128,26 +133,40 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& textToUpload = truncateLogForMclogs(text); } - std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting)); - - dialog.execWithTask(paste.get()); - if (!paste->wasSuccessful()) { - CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"), paste->failReason(), QMessageBox::Critical)->exec(); - return QString(); - } else { - const QString link = paste->pasteLink(); - setClipboardText(link); + auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network())); + + auto pasteJob = new PasteUpload(textToUpload, baseURL, pasteType); + job->addNetAction(Net::NetRequest::Ptr(pasteJob)); + QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show(); + }); + QObject::connect(job.get(), &Task::aborted, [parentWidget] { + CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"), + QObject::tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + if (dialog.execWithTask(job.get()) == QDialog::Accepted) { + if (pasteJob->pasteLink().isEmpty()) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty", + QMessageBox::Critical) + ->show(); + return {}; + } + setClipboardText(pasteJob->pasteLink()); CustomMessageBox::selectable( parentWidget, QObject::tr("Upload finished"), - QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(link), + QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(pasteJob->pasteLink()), QMessageBox::Information) ->exec(); - return link; + return pasteJob->pasteLink(); } + return {}; } -void GuiUtil::setClipboardText(const QString& text) +void GuiUtil::setClipboardText(QString text) { + anonymizeLog(text); QApplication::clipboard()->setText(text); } @@ -162,7 +181,7 @@ static QStringList BrowseForFileInternal(QString context, QFileDialog w(parentWidget, caption); QSet locations; - auto f = [&](QStandardPaths::StandardLocation l) { + auto f = [&locations](QStandardPaths::StandardLocation l) { QString location = QStandardPaths::writableLocation(l); QFileInfo finfo(location); if (!finfo.exists()) { diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 8d384d3f6..c3ba01f5b 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include namespace GuiUtil { -std::optional uploadPaste(const QString& name, const QString& text, QWidget* parentWidget); -void setClipboardText(const QString& text); +std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget); +std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget); +void setClipboardText(QString text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); } // namespace GuiUtil diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index bf83a56c9..a164351b0 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -37,7 +37,6 @@ #include "InstanceWindow.h" #include "Application.h" -#include #include #include #include @@ -50,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); @@ -76,7 +75,7 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin { auto horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); - horizontalLayout->setContentsMargins(6, -1, 6, -1); + horizontalLayout->setContentsMargins(0, 0, 6, 6); auto btnHelp = new QPushButton(this); btnHelp->setText(tr("Help")); @@ -110,15 +109,15 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin m_container->addButtons(horizontalLayout); - connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceWindow::updateButtons); + connect(m_instance, &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); + connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceWindow::updateButtons); } // restore window state { - auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toByteArray(); + auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toString().toUtf8(); restoreState(QByteArray::fromBase64(base64State)); - auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toByteArray(); + auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toString().toUtf8(); restoreGeometry(QByteArray::fromBase64(base64Geometry)); } @@ -126,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 @@ -165,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; } @@ -190,8 +189,8 @@ void InstanceWindow::closeEvent(QCloseEvent* event) return; } - APPLICATION->settings()->set("ConsoleWindowState", saveState().toBase64()); - APPLICATION->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("ConsoleWindowState", QString::fromUtf8(saveState().toBase64())); + APPLICATION->settings()->set("ConsoleWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); emit isClosing(); event->accept(); } 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 5b3eda7c4..96dfda751 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -71,6 +71,7 @@ #include #include #include +#include #include #include @@ -90,8 +91,11 @@ #include #include "InstanceWindow.h" +#include "ui/GuiUtil.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" +#include "ui/dialogs/CreateShortcutDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" @@ -123,6 +127,7 @@ #include "KonamiCode.h" #include "InstanceCopyTask.h" +#include "InstanceDirUpdate.h" #include "Json.h" @@ -143,7 +148,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi { ui->setupUi(this); - setWindowIcon(APPLICATION->getThemedIcon("logo")); + connect(ui->actionThemes, &QAction::triggered, this, []() { + DesktopServices::openUrl(QUrl(BuildConfig.THEMES_URL)); + }); + + setWindowIcon(APPLICATION->logo()); setWindowTitle(APPLICATION->applicationDisplayName()); #ifndef QT_NO_ACCESSIBILITY setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); @@ -154,13 +163,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // Qt doesn't like vertical moving toolbars, so we have to force them... // See https://github.com/PolyMC/PolyMC/issues/493 connect(ui->instanceToolBar, &QToolBar::orientationChanged, - [=](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); + [this](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); // if you try to add a widget to a toolbar in a .ui file // qt designer will delete it when you save the file >:( changeIconButton = new LabeledToolButton(this); changeIconButton->setObjectName(QStringLiteral("changeIconButton")); - changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); + changeIconButton->setIcon(QIcon::fromTheme("news")); changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered); ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton); @@ -175,12 +184,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // restore the instance toolbar settings auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); - if (!APPLICATION->settings()->contains(setting_name)) - instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name); - else - instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); + instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); + ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toString().toUtf8())); ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); @@ -213,10 +219,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // hide, disable and show stuff { ui->actionReportBug->setVisible(!BuildConfig.BUG_TRACKER_URL.isEmpty()); - ui->actionTHEMES->setVisible(!BuildConfig.THEMES_URL.isEmpty()); - // ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty()); - // ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); - // ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); + ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty()); + ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); + ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); ui->actionCheckUpdate->setVisible(APPLICATION->updaterEnabled()); @@ -238,6 +243,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } + { // logs viewing + connect(ui->actionViewLog, &QAction::triggered, this, [] { APPLICATION->showLogWindow(); }); + } + // add the toolbar toggles to the view menu ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); @@ -272,14 +281,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi { m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); newsLabel = new QToolButton(); - newsLabel->setIcon(APPLICATION->getThemedIcon("news")); + newsLabel->setIcon(QIcon::fromTheme("news")); newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsLabel->setFocusPolicy(Qt::NoFocus); ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); - QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); - QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); + connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); + connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); updateNewsLabel(); } @@ -289,10 +298,27 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi view->setSelectionMode(QAbstractItemView::SingleSelection); // FIXME: leaks ListViewDelegate - view->setItemDelegate(new ListViewDelegate(this)); + auto delegate = new ListViewDelegate(this); + view->setItemDelegate(delegate); view->setFrameShape(QFrame::NoFrame); // do not show ugly blue border on the mac view->setAttribute(Qt::WA_MacShowFocusRect, false); + connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) { + if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) { + auto oldID = m_selectedInstance->id(); + auto newID = QFileInfo(newRoot).fileName(); + QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID)); + bool syncGroup = origGroup != GroupId() && oldID != newID; + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(oldID, GroupId()); + + refreshInstances(); + setSelectedInstanceById(newID); + + if (syncGroup) + APPLICATION->instances()->setInstanceGroup(newID, origGroup); + } + }); view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); @@ -300,14 +326,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 @@ -343,16 +369,16 @@ 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::globalSettingsClosed, this, &MainWindow::globalSettingsClosed); + connect(APPLICATION, &Application::globalSettingsApplied, this, &MainWindow::globalSettingsClosed); m_statusLeft = new QLabel(tr("No instance selected"), this); m_statusCenter = new QLabel(tr("Total playtime: 0s"), this); @@ -372,8 +398,8 @@ 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::listChanged, [this] { defaultAccountChanged(); }); // Show initial account defaultAccountChanged(); @@ -397,7 +423,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); } } @@ -541,7 +567,7 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) actionCreateInstance->setData(instance_action_data); } - connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered())); + connect(actionCreateInstance, &QAction::triggered, this, &MainWindow::on_actionAddInstance_triggered); actions.prepend(actionSep); actions.prepend(actionVoid); @@ -662,7 +688,7 @@ void MainWindow::repopulateAccountsMenu() if (!face.isNull()) { action->setIcon(face); } else { - action->setIcon(APPLICATION->getThemedIcon("noaccount")); + action->setIcon(QIcon::fromTheme("noaccount")); } const int highestNumberKey = 9; @@ -671,7 +697,7 @@ void MainWindow::repopulateAccountsMenu() } ui->accountsMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + connect(action, &QAction::triggered, this, &MainWindow::changeActiveAccount); } } @@ -683,7 +709,7 @@ void MainWindow::repopulateAccountsMenu() ui->accountsMenu->addAction(ui->actionNoDefaultAccount); - connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + connect(ui->actionNoDefaultAccount, &QAction::triggered, this, &MainWindow::changeActiveAccount); ui->accountsMenu->addSeparator(); ui->accountsMenu->addAction(ui->actionManageAccounts); @@ -707,7 +733,7 @@ void MainWindow::changeActiveAccount() QAction* sAction = (QAction*)sender(); // Profile's associated Mojang username - if (sAction->data().type() != QVariant::Type::Int) + if (sAction->data().typeId() != QMetaType::Int) return; QVariant action_data = sAction->data(); @@ -733,7 +759,7 @@ void MainWindow::defaultAccountChanged() ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if (face.isNull()) { - ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); } else { ui->actionAccountsButton->setIcon(face); } @@ -741,7 +767,7 @@ void MainWindow::defaultAccountChanged() } // Set the icon to the "no account" icon. - ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); ui->actionAccountsButton->setText(tr("Accounts")); } @@ -797,11 +823,7 @@ void MainWindow::updateNewsLabel() QList stringToIntList(const QString& string) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList split = string.split(',', Qt::SkipEmptyParts); -#else - QStringList split = string.split(',', QString::SkipEmptyParts); -#endif QList out; for (int i = 0; i < split.size(); ++i) { out.append(split.at(i).toInt()); @@ -942,14 +964,14 @@ void MainWindow::processURLs(QList urls) auto array = std::make_shared(); auto api = FlameAPI(); - auto job = api.getFile(addonId, fileId, array); + auto job = api.getFile(addonId, fileId, array.get()); connect(job.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] { qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); auto doc = Json::requireDocument(*array); - auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); + auto data = doc.object()["data"].toObject(); // No way to find out if it's a mod or a modpack before here // And also we need to check if it ends with .zip, instead of any better way version = FlameMod::loadIndexedPackVersion(data); @@ -1023,7 +1045,7 @@ void MainWindow::processURLs(QList urls) auto type = ResourceUtils::identify(localFileInfo); - if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack + if (ModPlatform::ResourceTypeUtils::VALID_RESOURCES.count(type) == 0) { // probably instance/modpack addInstance(localFileName, extra_info); continue; } @@ -1044,28 +1066,28 @@ 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 PackedResourceType::ResourcePack: - minecraftInst->resourcePackList()->installResource(localFileName); + case ModPlatform::ResourceType::ResourcePack: + minecraftInst->resourcePackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::TexturePack: - minecraftInst->texturePackList()->installResource(localFileName); + case ModPlatform::ResourceType::TexturePack: + minecraftInst->texturePackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::DataPack: + case ModPlatform::ResourceType::DataPack: qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; - case PackedResourceType::Mod: - minecraftInst->loaderModList()->installMod(localFileName, version); + case ModPlatform::ResourceType::Mod: + minecraftInst->loaderModList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::ShaderPack: - minecraftInst->shaderPackList()->installResource(localFileName); + case ModPlatform::ResourceType::ShaderPack: + minecraftInst->shaderPackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::WorldSave: + case ModPlatform::ResourceType::World: minecraftInst->worldList()->installWorld(localFileInfo); break; - case PackedResourceType::UNKNOWN: + case ModPlatform::ResourceType::Unknown: default: qDebug() << "Can't Identify" << localFileName << "Ignoring it."; break; @@ -1083,11 +1105,6 @@ void MainWindow::on_actionDISCORD_triggered() DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL)); } -void MainWindow::on_actionTHEMES_triggered() -{ - DesktopServices::openUrl(QUrl(BuildConfig.THEMES_URL)); -} - void MainWindow::on_actionMATRIX_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.MATRIX_URL)); @@ -1189,7 +1206,10 @@ void MainWindow::renameGroup(QString group) void MainWindow::undoTrashInstance() { - APPLICATION->instances()->undoTrashInstance(); + if (!APPLICATION->instances()->undoTrashInstance()) + QMessageBox::warning( + this, tr("Failed to undo trashing instance"), + tr("Some instances and shortcuts could not be restored.\nPlease check your trashbin to manually restore them.")); ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); } @@ -1275,7 +1295,7 @@ void MainWindow::globalSettingsClosed() updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but Prism Launcher exits abnormally, causing the window state to never be saved: - APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); + APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); update(); } @@ -1306,7 +1326,15 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { - APPLICATION->metacache()->evictAll(); + // This if contains side effects! + if (!APPLICATION->metacache()->evictAll()) { + CustomMessageBox::selectable(this, tr("Error"), + tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " + "Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), + QMessageBox::Warning) + ->show(); + } + APPLICATION->metacache()->SaveNow(); } @@ -1370,33 +1398,33 @@ void MainWindow::on_actionDeleteInstance_triggered() return; } + if (m_selectedInstance->isRunning()) { + CustomMessageBox::selectable(this, tr("Cannot Delete Running Instance"), + tr("The selected instance is currently running and cannot be deleted. Please stop the instance before " + "attempting to delete it."), + QMessageBox::Warning, QMessageBox::Ok) + ->exec(); + return; + } auto id = m_selectedInstance->id(); + QString shortcutStr; + auto shortcuts = m_selectedInstance->shortcuts(); + if (!shortcuts.isEmpty()) + shortcutStr = tr(" and its %n registered shortcut(s)", "", shortcuts.size()); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), - tr("You are about to delete \"%1\".\n" + tr("You are about to delete \"%1\"%2.\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") - .arg(m_selectedInstance->name()), + .arg(m_selectedInstance->name(), shortcutStr), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; - auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); - if (!linkedInstances.empty()) { - response = CustomMessageBox::selectable(this, tr("There are linked instances"), - tr("The following instance(s) might reference files in this instance:\n\n" - "%1\n\n" - "Deleting it could break the other instance(s), \n\n" - "Do you wish to proceed?", - nullptr, linkedInstances.count()) - .arg(linkedInstances.join("\n")), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - if (response != QMessageBox::Yes) - return; - } + if (!checkLinkedInstances(id, this, tr("Deleting"))) + return; if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); @@ -1418,15 +1446,18 @@ void MainWindow::on_actionExportInstanceZip_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered() { if (m_selectedInstance) { - ExportPackDialog dlg(m_selectedInstance, this); - dlg.exec(); + auto instance = dynamic_cast(m_selectedInstance); + if (instance != nullptr) { + ExportPackDialog dlg(instance, this); + dlg.exec(); + } } } void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { - auto instance = dynamic_cast(m_selectedInstance.get()); + auto instance = dynamic_cast(m_selectedInstance); if (instance) { if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { @@ -1435,7 +1466,7 @@ void MainWindow::on_actionExportInstanceFlamePack_triggered() msgBox.exec(); return; } - ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME); + ExportPackDialog dlg(instance, this, ModPlatform::ResourceProvider::FLAME); dlg.exec(); } } @@ -1459,9 +1490,9 @@ void MainWindow::on_actionViewSelectedInstFolder_triggered() void MainWindow::closeEvent(QCloseEvent* event) { // Save the window state and geometry. - APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); - APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); - instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState()); + APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); + APPLICATION->settings()->set("MainWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); + instanceToolbarSetting->set(QString::fromUtf8(ui->instanceToolBar->getVisibilityState().toBase64())); event->accept(); emit isClosing(); } @@ -1479,7 +1510,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; @@ -1493,7 +1524,7 @@ void MainWindow::on_actionLaunchInstance_triggered() } } -void MainWindow::activateInstance(InstancePtr instance) +void MainWindow::activateInstance(BaseInstance* instance) { APPLICATION->launch(instance); } @@ -1509,141 +1540,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() { if (!m_selectedInstance) return; - auto desktopPath = FS::getDesktopDir(); - if (desktopPath.isEmpty()) { - // TODO come up with an alternative solution (open "save file" dialog) - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } - - QString desktopFilePath; - QString appPath = QApplication::applicationFilePath(); - QString iconPath; - QStringList args; -#if defined(Q_OS_MACOS) - appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } - - auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (pIcon == nullptr) { - pIcon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + CreateShortcutDialog shortcutDlg(m_selectedInstance, this); + if (!shortcutDlg.exec()) return; - } - - QIcon icon = pIcon->icon(); - - bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } else if (appPath.endsWith("/")) { - appPath.chop(1); - } - } - - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - - if (DesktopServices::isFlatpak()) { - desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(desktopPath); - desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)"); - if (desktopFilePath.isEmpty()) - return; // file dialog canceled by user - appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); - } - -#elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); - - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - -#else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); - return; -#endif - args.append({ "--launch", m_selectedInstance->id() }); - if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { -#if not defined(Q_OS_MACOS) - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); -#else - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); -#endif - } else { -#if not defined(Q_OS_MACOS) - iconFile.remove(); -#endif - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } + shortcutDlg.createShortcut(); } void MainWindow::taskEnd() @@ -1657,8 +1558,8 @@ void MainWindow::taskEnd() void MainWindow::startTask(Task* task) { - connect(task, SIGNAL(succeeded()), SLOT(taskEnd())); - connect(task, SIGNAL(failed(QString)), SLOT(taskEnd())); + connect(task, &Task::succeeded, this, &MainWindow::taskEnd); + connect(task, &Task::failed, this, &MainWindow::taskEnd); task->start(); } @@ -1670,8 +1571,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); @@ -1691,8 +1592,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 48f7f214a..7dcba885a 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -100,8 +100,6 @@ class MainWindow : public QMainWindow { void on_actionMATRIX_triggered(); - void on_actionTHEMES_triggered(); - void on_actionDISCORD_triggered(); void on_actionCopyInstance_triggered(); @@ -222,7 +220,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); @@ -249,7 +247,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 ead4a2c8f..50db7d146 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 27 + 22 @@ -215,10 +215,13 @@ + - - + + + + @@ -234,11 +237,10 @@ - - .. + - More news... + More News... Open the development blog to read more news about %1. @@ -249,8 +251,7 @@ true - - .. + &Meow @@ -285,8 +286,7 @@ - - .. + Add Instanc&e... @@ -297,8 +297,7 @@ - - .. + &Update... @@ -312,8 +311,7 @@ - - .. + Setti&ngs... @@ -327,8 +325,7 @@ - - .. + &Manage Accounts... @@ -336,8 +333,7 @@ - - .. + &Launch @@ -348,8 +344,7 @@ - - .. + &Kill @@ -363,8 +358,7 @@ - - .. + Rename @@ -375,8 +369,7 @@ - - .. + &Change Group... @@ -398,8 +391,7 @@ - - .. + &Edit... @@ -413,8 +405,7 @@ - - .. + &Folder @@ -425,8 +416,7 @@ - - .. + Dele&te @@ -440,8 +430,7 @@ - - .. + Cop&y... @@ -455,8 +444,7 @@ - - .. + E&xport... @@ -467,8 +455,7 @@ - - .. + Prism Launcher (zip) @@ -476,8 +463,7 @@ - - .. + Modrinth (mrpack) @@ -485,8 +471,7 @@ - - .. + CurseForge (zip) @@ -494,20 +479,18 @@ - - .. + Create Shortcut - Creates a shortcut on your desktop to launch the selected instance. + Creates a shortcut on a selected folder to launch the selected instance. - - .. + No accounts added! @@ -518,8 +501,7 @@ true - - .. + No Default Account @@ -530,8 +512,7 @@ - - .. + Close &Window @@ -545,8 +526,7 @@ - - .. + &Instances @@ -557,8 +537,7 @@ - - .. + Launcher &Root @@ -569,8 +548,7 @@ - - .. + &Central Mods @@ -581,8 +559,7 @@ - - .. + &Skins @@ -593,8 +570,7 @@ - - .. + Instance Icons @@ -605,8 +581,7 @@ - - .. + Logs @@ -620,34 +595,53 @@ Themes - + - - .. + - &Themes Guide + Report a Bug or Suggest a Feature - Open the themes guide to learn how to customize %1. + Open the bug tracker to report a bug with %1. - + - - .. + - Report a Bug or Suggest a Feature + &Discord Guild - Open the bug tracker to report a bug with %1. + Open %1 Discord guild. + + + + + + + + &Matrix Space + + + Open %1 Matrix space. + + + + + + + + Sub&reddit + + + Open %1 subreddit. - - .. + &About %1 @@ -661,8 +655,7 @@ - - .. + &Clear Metadata Cache @@ -671,11 +664,22 @@ Clear cached metadata - + - + .. + + View logs + + + View current and previous launcher logs + + + + + + Install to &PATH @@ -685,8 +689,7 @@ - - .. + Folders @@ -697,8 +700,7 @@ - - .. + Help @@ -709,8 +711,7 @@ - - .. + Accounts @@ -718,8 +719,7 @@ - - .. + %1 &Help @@ -730,8 +730,7 @@ - - .. + &Widget Themes @@ -742,8 +741,7 @@ - - .. + I&con Theme @@ -754,8 +752,7 @@ - - .. + Cat Packs @@ -766,8 +763,7 @@ - - .. + Java @@ -776,6 +772,11 @@ Open the Java folder in a file browser. Only available if the built-in Java downloader is used. + + + + + diff --git a/launcher/ui/ToolTipFilter.cpp b/launcher/ui/ToolTipFilter.cpp new file mode 100644 index 000000000..367c39230 --- /dev/null +++ b/launcher/ui/ToolTipFilter.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Mark Deneen + * + * 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 "ToolTipFilter.h" + +bool ToolTipFilter::eventFilter(QObject* obj, QEvent* ev) +{ + if (ev->type() == QEvent::ToolTip) { + return true; + } else { + return QObject::eventFilter(obj, ev); + } +} diff --git a/launcher/ui/ToolTipFilter.h b/launcher/ui/ToolTipFilter.h new file mode 100644 index 000000000..c5ab66206 --- /dev/null +++ b/launcher/ui/ToolTipFilter.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2026 Mark Deneen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class ToolTipFilter : public QObject { + Q_OBJECT + protected: + bool eventFilter(QObject* obj, QEvent* event); +}; diff --git a/launcher/ui/ViewLogWindow.cpp b/launcher/ui/ViewLogWindow.cpp new file mode 100644 index 000000000..9d7d7821f --- /dev/null +++ b/launcher/ui/ViewLogWindow.cpp @@ -0,0 +1,26 @@ +#include + +#include "ViewLogWindow.h" + +#include "ui/pages/instance/OtherLogsPage.h" + +ViewLogWindow::ViewLogWindow(QWidget* parent) + : QMainWindow(parent), m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) +{ + setAttribute(Qt::WA_DeleteOnClose); + setWindowIcon(QIcon::fromTheme("log")); + setWindowTitle(tr("View Launcher Logs")); + setCentralWidget(m_page); + setMinimumSize(m_page->size()); + setContentsMargins(6, 6, 0, 6); // the "Other Logs" instance page has 6px padding on the right, + // to have equal padding in all directions in the dialog we add it to all other sides. + m_page->opened(); + show(); +} + +void ViewLogWindow::closeEvent(QCloseEvent* event) +{ + m_page->closed(); + emit isClosing(); + event->accept(); +} diff --git a/launcher/ui/ViewLogWindow.h b/launcher/ui/ViewLogWindow.h new file mode 100644 index 000000000..bb10683aa --- /dev/null +++ b/launcher/ui/ViewLogWindow.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "Application.h" + +class OtherLogsPage; + +class ViewLogWindow : public QMainWindow { + Q_OBJECT + + public: + explicit ViewLogWindow(QWidget* parent = nullptr); + + signals: + void isClosing(); + + protected: + void closeEvent(QCloseEvent*) override; + + private: + OtherLogsPage* m_page; +}; diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index e1d4946e1..6052e94a9 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -42,102 +42,33 @@ #include "ui_AboutDialog.h" #include -#include namespace { -QString getLink(QString link, QString name) -{ - return QString("<%2>").arg(link).arg(name); -} - -QString getWebsite(QString link) -{ - return getLink(link, QObject::tr("Website")); -} - -QString getGitHub(QString username) -{ - return getLink("https://github.com/" + username, "GitHub"); -} - -// Credits -// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument... QString getCreditsHtml() { - QString output; - QTextStream stream(&output); -#if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) - stream.setCodec(QTextCodec::codecForName("UTF-8")); -#endif - stream << "
    \n"; - - //: %1 is the name of the launcher, determined at build time, e.g. "NMC Launcher Developers" - stream << "

    " << QObject::tr("%1 Developers", "About Credits").arg(BuildConfig.LAUNCHER_DISPLAYNAME) << "

    \n"; - stream << QString("

    sogik %1

    \n").arg(getGitHub("sogik")); - stream << "
    \n"; - - //: %1 is the name of the launcher, determined at build time, e.g. "`POLLYMC Developers" - stream << "

    " << QObject::tr("%1 Developers", "About Credits").arg("PollyMC") << "

    \n"; - stream << QString("

    fn2006 %1

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

    Evan Goode %1

    \n").arg(getGitHub("evan-goode")); - stream << "
    \n"; - - //: %1 is the name of the launcher, determined at build time, e.g. "Prism Launcher Developers" - stream << "

    " << QObject::tr("%1 Developers", "About Credits").arg("Prism Launcher") << "

    \n"; - stream << QString("

    Sefa Eyeoglu (Scrumplex) %1

    \n").arg(getWebsite("https://scrumplex.net")); - stream << QString("

    d-513 %1

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

    txtsd %1

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

    timoreo %1

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

    ZekeZ %1

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

    cozyGalvinism %1

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

    DioEgizio %1

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

    flowln %1

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

    ViRb3 %1

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

    Rachel Powers (Ryex) %1

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

    TayouVR %1

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

    TheKodeToad %1

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

    getchoo %1

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

    Alexandru Tripon (Trial97) %1

    \n").arg(getGitHub("Trial97")); - stream << "
    \n"; - - // TODO: possibly retrieve from git history at build time? - //: %1 is the name of the launcher, determined at build time, e.g. "MULTIMC Developers" - stream << "

    " << QObject::tr("%1 Developers", "About Credits").arg("MultiMC") << "

    \n"; - stream << "

    Andrew Okin <forkk@forkk.net>

    \n"; - stream << QString("

    Petr Mrázek <peterix@gmail.com>

    \n"); - stream << "

    Sky Welch <multimc@bunnies.io>

    \n"; - stream << "

    Jan (02JanDal) <02jandal@gmail.com>

    \n"; - stream << "

    RoboSky <@RoboSky_>

    \n"; - stream << "
    \n"; - - stream << "

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

    \n"; - stream << QString("

    Boba %1

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

    AutiOne %1

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

    Fulmine %1

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

    ely %1

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

    gon sawa %1

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

    Pankakes

    \n"); - stream << QString("

    tobimori %1

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

    Orochimarufan <orochimarufan.x3@gmail.com>

    \n"; - stream << "

    TakSuyu <taksuyu@gmail.com>

    \n"; - stream << "

    Kilobyte <stiepen22@gmx.de>

    \n"; - stream << "

    Rootbear75 <@rootbear75>

    \n"; - stream << "

    Zeker Zhayard <@Zeker_Zhayard>

    \n"; - stream << "

    Everyone who helped establish our branding!

    \n"; - stream - << "

    And everyone else who contributed!

    \n"; - stream << "
    \n"; - - stream << "
    \n"; - return output; + QFile dataFile(":/documents/credits.html"); + if (!dataFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + return {}; + } + QString fileContent = QString::fromUtf8(dataFile.readAll()); + dataFile.close(); + + return fileContent.arg(QObject::tr("%1 Developers").arg(BuildConfig.LAUNCHER_DISPLAYNAME), QObject::tr("MultiMC Developers"), + QObject::tr("With special thanks to")); } QString getLicenseHtml() { QFile dataFile(":/documents/COPYING.md"); - dataFile.open(QIODevice::ReadOnly); - QString output = markdownToHTML(dataFile.readAll()); - return output; + if (dataFile.open(QIODevice::ReadOnly)) { + QString output = markdownToHTML(dataFile.readAll()); + dataFile.close(); + return output; + } else { + qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; + return QString(); + } } } // namespace @@ -158,7 +89,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia ui->urlLabel->setOpenExternalLinks(true); - ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); + ui->icon->setPixmap(APPLICATION->logo().pixmap(64)); ui->title->setText(launcherName); ui->versionLabel->setText(BuildConfig.printableVersionString()); @@ -188,7 +119,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT); - connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); + connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close); connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); } diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index b3b6d2bcc..3691cbcd4 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -43,21 +43,18 @@ #include BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type) - : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) + : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hashType(hash_type) { - m_hashing_task = shared_qobject_ptr( + m_hashingTask = shared_qobject_ptr( new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); - connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); + connect(m_hashingTask.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); - connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); - - auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); - connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); + connect(ui->openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); + connect(ui->downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); @@ -174,10 +171,12 @@ void BlockedModsDialog::update() if (allModsMatched()) { ui->labelModsFound->setText("" + tr("All mods found")); - m_openMissingButton->setDisabled(true); + ui->openMissingButton->setDisabled(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); - m_openMissingButton->setDisabled(false); + ui->openMissingButton->setDisabled(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Skip")); } } @@ -260,7 +259,7 @@ void BlockedModsDialog::scanPath(QString path, bool start_task) void BlockedModsDialog::addHashTask(QString path) { qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; - m_pending_hash_paths.insert(path); + m_pendingHashPaths.insert(path); } /// @brief add a hashing task for the file located at path and connect it to check that hash against @@ -268,14 +267,14 @@ void BlockedModsDialog::addHashTask(QString path) /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { - auto hash_task = Hashing::createHasher(path, m_hash_type); + auto hash_task = Hashing::createHasher(path, m_hashType); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; connect(hash_task.get(), &Task::succeeded, this, [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; }); - m_hashing_task->addTask(hash_task); + m_hashingTask->addTask(hash_task); } /// @brief check if the computed hash for the provided path matches a blocked @@ -288,6 +287,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path; + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (mod.matched) { continue; @@ -295,6 +296,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path) if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { mod.matched = true; mod.localPath = path; + if (moveFiles) { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } match = true; qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; @@ -346,6 +350,8 @@ bool BlockedModsDialog::checkValidPath(QString path) return fsName.compare(metaName) == 0; }; + auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); + auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (compare(filename, mod.name)) { // if the mod is not yet matched and doesn't have a hash then @@ -353,6 +359,9 @@ bool BlockedModsDialog::checkValidPath(QString path) if (!mod.matched && mod.hash.isEmpty()) { mod.matched = true; mod.localPath = path; + if (moveFiles) { + mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); + } return false; } qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; @@ -396,31 +405,31 @@ void BlockedModsDialog::validateMatchedMods() /// @brief run hash task or mark a pending run if it is already running void BlockedModsDialog::runHashTask() { - if (!m_hashing_task->isRunning()) { - m_rehash_pending = false; + if (!m_hashingTask->isRunning()) { + m_rehashPending = false; - if (!m_pending_hash_paths.isEmpty()) { + if (!m_pendingHashPaths.isEmpty()) { qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; - auto path = m_pending_hash_paths.begin(); - while (path != m_pending_hash_paths.end()) { + auto path = m_pendingHashPaths.begin(); + while (path != m_pendingHashPaths.end()) { buildHashTask(*path); - path = m_pending_hash_paths.erase(path); + path = m_pendingHashPaths.erase(path); } - m_hashing_task->start(); + m_hashingTask->start(); } } else { qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; - qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pending_hash_paths; - m_rehash_pending = true; + qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pendingHashPaths; + m_rehashPending = true; } } void BlockedModsDialog::hashTaskFinished() { qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; - if (m_rehash_pending) { + if (m_rehashPending) { qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; runHashTask(); } diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index 09722bce9..15d4d4770 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -42,6 +42,8 @@ struct BlockedMod { bool matched; QString localPath; QString targetFolder; + bool disabled = false; + bool move = false; }; QT_BEGIN_NAMESPACE @@ -69,11 +71,10 @@ class BlockedModsDialog : public QDialog { Ui::BlockedModsDialog* ui; QList& m_mods; QFileSystemWatcher m_watcher; - shared_qobject_ptr m_hashing_task; - QSet m_pending_hash_paths; - bool m_rehash_pending; - QPushButton* m_openMissingButton; - QString m_hash_type; + shared_qobject_ptr m_hashingTask; + QSet m_pendingHashPaths; + bool m_rehashPending; + QString m_hashType; void openAll(bool missingOnly); void addDownloadFolder(); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index 2292b99c0..850ad713e 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -6,20 +6,26 @@ 0 0 - 400 - 400 + 800 + 500 + + + 2 + 1 + + - 0 + 700 350 BlockedModsDialog - + @@ -36,47 +42,106 @@ - <html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p></body></html> + <html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p><p><span style=" font-weight:600;">Click 'Open Missing' to open all the download links in the browser. </span></p></body></html> true - - true - - - - - - - true - - - true - - - - Watched Folders: - - - - - - - - 0 - 12 - - - - true - - - false + + + 0 + + + Blocked Mods + + + + + + true + + + true + + + + + + + + + Open Missing + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Watched Folders + + + + + + + 0 + 12 + + + + true + + + false + + + + + + + + + Add Download Folder + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + diff --git a/launcher/ui/dialogs/ChooseOfflineNameDialog.cpp b/launcher/ui/dialogs/ChooseOfflineNameDialog.cpp new file mode 100644 index 000000000..35b8faffb --- /dev/null +++ b/launcher/ui/dialogs/ChooseOfflineNameDialog.cpp @@ -0,0 +1,75 @@ +// 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 "ChooseOfflineNameDialog.h" + +#include +#include + +#include "ui_ChooseOfflineNameDialog.h" + +ChooseOfflineNameDialog::ChooseOfflineNameDialog(const QString& message, QWidget* parent) + : QDialog(parent), ui(new Ui::ChooseOfflineNameDialog) +{ + ui->setupUi(this); + ui->label->setText(message); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + const QRegularExpression usernameRegExp("^[A-Za-z0-9_]{3,16}$"); + m_usernameValidator = new QRegularExpressionValidator(usernameRegExp, this); + ui->usernameTextBox->setValidator(m_usernameValidator); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +ChooseOfflineNameDialog::~ChooseOfflineNameDialog() +{ + delete ui; +} + +QString ChooseOfflineNameDialog::getUsername() const +{ + return ui->usernameTextBox->text(); +} + +void ChooseOfflineNameDialog::setUsername(const QString& username) const +{ + ui->usernameTextBox->setText(username); + updateAcceptAllowed(username); +} + +void ChooseOfflineNameDialog::updateAcceptAllowed(const QString& username) const +{ + const bool allowed = ui->allowInvalidUsernames->isChecked() ? !username.isEmpty() : ui->usernameTextBox->hasAcceptableInput(); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowed); +} + +void ChooseOfflineNameDialog::on_usernameTextBox_textEdited(const QString& newText) const +{ + updateAcceptAllowed(newText); +} + +void ChooseOfflineNameDialog::on_allowInvalidUsernames_checkStateChanged(const Qt::CheckState checkState) const +{ + ui->usernameTextBox->setValidator(checkState == Qt::Checked ? nullptr : m_usernameValidator); + updateAcceptAllowed(getUsername()); +} diff --git a/launcher/ui/dialogs/ChooseOfflineNameDialog.h b/launcher/ui/dialogs/ChooseOfflineNameDialog.h new file mode 100644 index 000000000..ce3c24ef9 --- /dev/null +++ b/launcher/ui/dialogs/ChooseOfflineNameDialog.h @@ -0,0 +1,50 @@ +// 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 +#include + +QT_BEGIN_NAMESPACE +namespace Ui { +class ChooseOfflineNameDialog; +} +QT_END_NAMESPACE + +class ChooseOfflineNameDialog final : public QDialog { + Q_OBJECT + + public: + explicit ChooseOfflineNameDialog(const QString& message, QWidget* parent = nullptr); + ~ChooseOfflineNameDialog() override; + + QString getUsername() const; + void setUsername(const QString& username) const; + + private: + void updateAcceptAllowed(const QString& username) const; + + protected slots: + void on_usernameTextBox_textEdited(const QString& newText) const; + void on_allowInvalidUsernames_checkStateChanged(Qt::CheckState checkState) const; + + private: + Ui::ChooseOfflineNameDialog* ui; + QRegularExpressionValidator* m_usernameValidator; +}; diff --git a/launcher/ui/dialogs/ChooseOfflineNameDialog.ui b/launcher/ui/dialogs/ChooseOfflineNameDialog.ui new file mode 100644 index 000000000..51a10e5b8 --- /dev/null +++ b/launcher/ui/dialogs/ChooseOfflineNameDialog.ui @@ -0,0 +1,58 @@ + + + ChooseOfflineNameDialog + + + + 0 + 0 + 400 + 158 + + + + Choose Offline Name + + + + + + + 0 + 0 + + + + Message label placeholder. + + + + + + + Username + + + + + + + A username is valid only if it is from 3 to 16 characters in length, uses English letters, numbers, and underscores. An invalid username may prevent joining servers and singleplayer worlds. + + + Allow invalid usernames + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + 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 new file mode 100644 index 000000000..7d4199f13 --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "BuildConfig.h" +#include "CreateShortcutDialog.h" +#include "ui_CreateShortcutDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseInstance.h" +#include "DesktopServices.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "icons/IconList.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/ShortcutUtils.h" +#include "minecraft/WorldList.h" +#include "minecraft/auth/AccountList.h" + +CreateShortcutDialog::CreateShortcutDialog(BaseInstance* instance, QWidget* parent) + : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) +{ + ui->setupUi(this); + + InstIconKey = instance->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setPlaceholderText(instance->name()); + + auto mInst = dynamic_cast(instance); + m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + auto worldList = mInst->worldList(); + worldList->update(); + if (!m_QuickJoinSupported || worldList->empty()) { + ui->worldTarget->hide(); + ui->worldSelectionBox->hide(); + ui->serverTarget->setChecked(true); + ui->serverTarget->hide(); + ui->serverLabel->show(); + } + + // Populate save targets + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!desktopDir.isEmpty()) + ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(ShortcutTarget::Desktop)); + + if (!applicationDir.isEmpty()) + ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(ShortcutTarget::Applications)); + } + ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(ShortcutTarget::Other)); + + // Populate worlds + if (m_QuickJoinSupported) { + for (const auto& world : worldList->allWorlds()) { + // Entry name: World Name [Game Mode] - Last Played: DateTime + QString entry_name = tr("%1 [%2] - Last Played: %3") + .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate)); + ui->worldSelectionBox->addItem(entry_name, world.name()); + } + } + + // Populate accounts + auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + if (accounts->count() <= 0) { + ui->overrideAccountCheckbox->setEnabled(false); + } else { + for (int i = 0; i < accounts->count(); i++) { + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = account->profileName(); + if (account->isInUse()) + profileLabel = tr("%1 (in use)").arg(profileLabel); + auto face = account->getFace(); + QIcon icon = face.isNull() ? QIcon::fromTheme("noaccount") : face; + ui->accountSelectionBox->addItem(profileLabel, account->profileName()); + ui->accountSelectionBox->setItemIcon(i, icon); + if (defaultAccount == account) + ui->accountSelectionBox->setCurrentIndex(i); + } + } +} + +CreateShortcutDialog::~CreateShortcutDialog() +{ + delete ui; +} + +void CreateShortcutDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) +{ + ui->accountOptionsGroup->setEnabled(state == Qt::Checked); +} + +void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) +{ + ui->targetOptionsGroup->setEnabled(state == Qt::Checked); + ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked()); + ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked()); + stateChanged(); +} + +void CreateShortcutDialog::on_worldTarget_toggled(bool checked) +{ + ui->worldSelectionBox->setEnabled(checked); + stateChanged(); +} + +void CreateShortcutDialog::on_serverTarget_toggled(bool checked) +{ + ui->serverAddressBox->setEnabled(checked); + stateChanged(); +} + +void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) +{ + stateChanged(); +} + +void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text) +{ + stateChanged(); +} + +void CreateShortcutDialog::stateChanged() +{ + QString result = m_instance->name(); + if (ui->targetCheckbox->isChecked()) { + if (ui->worldTarget->isChecked()) + result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString()); + else if (ui->serverTarget->isChecked()) + result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text()); + } + ui->instNameTextBox->setPlaceholderText(result); + if (!ui->targetCheckbox->isChecked()) + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + else { + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) || + (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); + } +} + +// Real work +void CreateShortcutDialog::createShortcut() +{ + QString targetString = tr("instance"); + QStringList extraArgs; + if (ui->targetCheckbox->isChecked()) { + if (ui->worldTarget->isChecked()) { + targetString = tr("world"); + extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() }; + } else if (ui->serverTarget->isChecked()) { + targetString = tr("server"); + extraArgs = { "--server", ui->serverAddressBox->text() }; + } + } + + auto target = ui->saveTargetSelectionBox->currentData().value(); + auto name = ui->instNameTextBox->text(); + if (name.isEmpty()) + name = ui->instNameTextBox->placeholderText(); + if (ui->overrideAccountCheckbox->isChecked()) + extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); + + ShortcutUtils::Shortcut args{ m_instance, name, targetString, this, extraArgs, InstIconKey, target }; + if (target == ShortcutTarget::Desktop) + ShortcutUtils::createInstanceShortcutOnDesktop(args); + else if (target == ShortcutTarget::Applications) + ShortcutUtils::createInstanceShortcutInApplications(args); + else + ShortcutUtils::createInstanceShortcutInOther(args); +} diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h new file mode 100644 index 000000000..8d666ef63 --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -0,0 +1,59 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "BaseInstance.h" + +class BaseInstance; + +namespace Ui { +class CreateShortcutDialog; +} + +class CreateShortcutDialog : public QDialog { + Q_OBJECT + + public: + explicit CreateShortcutDialog(BaseInstance* instance, QWidget* parent = nullptr); + ~CreateShortcutDialog(); + + void createShortcut(); + + private slots: + // Icon, target and name + void on_iconButton_clicked(); + + // Override account + void on_overrideAccountCheckbox_stateChanged(int state); + + // Override target (world, server) + void on_targetCheckbox_stateChanged(int state); + void on_worldTarget_toggled(bool checked); + void on_serverTarget_toggled(bool checked); + void on_worldSelectionBox_currentIndexChanged(int index); + void on_serverAddressBox_textChanged(const QString& text); + + private: + // Data + Ui::CreateShortcutDialog* ui; + QString InstIconKey; + BaseInstance* m_instance; + bool m_QuickJoinSupported = false; + + // Functions + void stateChanged(); +}; diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui new file mode 100644 index 000000000..24d4dc2dc --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -0,0 +1,264 @@ + + + CreateShortcutDialog + + + Qt::WindowModality::ApplicationModal + + + + 0 + 0 + 450 + 370 + + + + Create Instance Shortcut + + + true + + + + + + + + + :/icons/instances/grass:/icons/instances/grass + + + + 80 + 80 + + + + + + + + + + Save To: + + + + + + + + 0 + 0 + + + + + + + + Name: + + + + + + + Name + + + + + + + + + + + Use a different account than the default specified. + + + Override the default account + + + + + + + false + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + + + + + + + Specify a world or server to automatically join on launch. + + + Select a target to join on launch + + + + + + + false + + + + 0 + 0 + + + + + + + 0 + + + + + World: + + + targetBtnGroup + + + + + + + + + + 0 + 0 + + + + + + + + 0 + + + + + Server Address: + + + targetBtnGroup + + + + + + + false + + + Server Address: + + + + + + + + + Server Address + + + + + + + + + + Note: If a shortcut is moved after creation, it won't be deleted when deleting the instance. + + + + + + + You'll need to delete them manually if that is the case. + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + iconButton + + + + + buttonBox + accepted() + CreateShortcutDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + CreateShortcutDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + + + + + + diff --git a/launcher/ui/dialogs/CustomMessageBox.cpp b/launcher/ui/dialogs/CustomMessageBox.cpp index 1af47a449..ca0fe99e0 100644 --- a/launcher/ui/dialogs/CustomMessageBox.cpp +++ b/launcher/ui/dialogs/CustomMessageBox.cpp @@ -21,7 +21,8 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) + QMessageBox::StandardButton defaultButton, + QCheckBox* checkBox) { QMessageBox* messageBox = new QMessageBox(parent); messageBox->setWindowTitle(title); @@ -31,6 +32,8 @@ QMessageBox* selectable(QWidget* parent, messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setIcon(icon); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + if (checkBox) + messageBox->setCheckBox(checkBox); return messageBox; } diff --git a/launcher/ui/dialogs/CustomMessageBox.h b/launcher/ui/dialogs/CustomMessageBox.h index a9bc6a24a..1ee29903e 100644 --- a/launcher/ui/dialogs/CustomMessageBox.h +++ b/launcher/ui/dialogs/CustomMessageBox.h @@ -23,5 +23,6 @@ QMessageBox* selectable(QWidget* parent, const QString& text, QMessageBox::Icon icon = QMessageBox::NoIcon, QMessageBox::StandardButtons buttons = QMessageBox::Ok, - QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton, + QCheckBox* checkBox = nullptr); } diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index d25cd32b6..96dab97e3 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -43,6 +43,7 @@ #include #include "FileIgnoreProxy.h" #include "QObjectPtr.h" +#include "archive/ExportToZipTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui_ExportInstanceDialog.h" @@ -59,45 +60,45 @@ #include "Application.h" #include "SeparatorPrefixTree.h" -ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) - : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +ExportInstanceDialog::ExportInstanceDialog(BaseInstance* instance, QWidget* parent) + : QDialog(parent), m_ui(new Ui::ExportInstanceDialog), m_instance(instance) { - ui->setupUi(this); + m_ui->setupUi(this); auto model = new QFileSystemModel(this); - model->setIconProvider(&icons); + model->setIconProvider(&m_icons); auto root = instance->instanceRoot(); - proxyModel = new FileIgnoreProxy(root, this); - proxyModel->setSourceModel(model); + m_proxyModel = new FileIgnoreProxy(root, this); + m_proxyModel->setSourceModel(model); auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); - proxyModel->ignoreFilesWithPath().insert({ FS::PathCombine(prefix, "logs"), FS::PathCombine(prefix, "crash-reports") }); - proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); - proxyModel->ignoreFilesWithPath().insert( - { FS::PathCombine(prefix, ".cache"), FS::PathCombine(prefix, ".fabric"), FS::PathCombine(prefix, ".quilt") }); - loadPackIgnore(); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { + m_proxyModel->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxyModel->loadBlockedPathsFromFile(ignoreFileName()); - ui->treeView->setModel(proxyModel); - ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); - ui->treeView->sortByColumn(0, Qt::AscendingOrder); + m_ui->treeView->setModel(m_proxyModel); + m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root))); + m_ui->treeView->sortByColumn(0, Qt::AscendingOrder); - connect(proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int))); + connect(m_proxyModel, &QAbstractItemModel::rowsInserted, this, &ExportInstanceDialog::rowsInserted); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); - auto headerView = ui->treeView->header(); + auto headerView = m_ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); - ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportInstanceDialog::~ExportInstanceDialog() { - delete ui; + delete m_ui; } /// Save icon to instance's folder is needed -void SaveIcon(InstancePtr m_instance) +void SaveIcon(BaseInstance* m_instance) { auto iconKey = m_instance->iconKey(); auto iconList = APPLICATION->icons(); @@ -144,13 +145,13 @@ void ExportInstanceDialog::doExport() auto files = QFileInfoList(); if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, - std::bind(&FileIgnoreProxy::filterFile, proxyModel, std::placeholders::_1))) { + std::bind(&FileIgnoreProxy::filterFile, m_proxyModel, std::placeholders::_1))) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); QDialog::done(QDialog::Rejected); return; } - auto task = makeShared(output, m_instance->instanceRoot(), files, "", true, true); + auto task = makeShared(output, m_instance->instanceRoot(), files, "", true); connect(task.get(), &Task::failed, this, [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); @@ -164,7 +165,7 @@ void ExportInstanceDialog::doExport() void ExportInstanceDialog::done(int result) { - savePackIgnore(); + m_proxyModel->saveBlockedPathsToFile(ignoreFileName()); if (result == QDialog::Accepted) { doExport(); return; @@ -176,13 +177,13 @@ void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) { // WARNING: possible off-by-one? for (int i = top; i < bottom; i++) { - auto node = proxyModel->index(i, 0, parent); - if (proxyModel->shouldExpand(node)) { + auto node = m_proxyModel->index(i, 0, parent); + if (m_proxyModel->shouldExpand(node)) { auto expNode = node.parent(); if (!expNode.isValid()) { continue; } - ui->treeView->expand(node); + m_ui->treeView->expand(node); } } } @@ -191,30 +192,3 @@ QString ExportInstanceDialog::ignoreFileName() { return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } - -void ExportInstanceDialog::loadPackIgnore() -{ - auto filename = ignoreFileName(); - QFile ignoreFile(filename); - if (!ignoreFile.open(QIODevice::ReadOnly)) { - return; - } - auto ignoreData = ignoreFile.readAll(); - auto string = QString::fromUtf8(ignoreData); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); -#else - proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); -#endif -} - -void ExportInstanceDialog::savePackIgnore() -{ - auto ignoreData = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); - auto filename = ignoreFileName(); - try { - FS::write(filename, ignoreData); - } catch (const Exception& e) { - qWarning() << e.cause(); - } -} diff --git a/launcher/ui/dialogs/ExportInstanceDialog.h b/launcher/ui/dialogs/ExportInstanceDialog.h index 183681f57..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,22 +52,20 @@ 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); private: void doExport(); - void loadPackIgnore(); - void savePackIgnore(); QString ignoreFileName(); private: - Ui::ExportInstanceDialog* ui; - InstancePtr m_instance; - FileIgnoreProxy* proxyModel; - FastFileIconProvider icons; + Ui::ExportInstanceDialog* m_ui; + BaseInstance* m_instance; + FileIgnoreProxy* m_proxyModel; + FastFileIconProvider m_icons; private slots: void rowsInserted(QModelIndex parent, int top, int bottom); diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index ed79a741f..d0a9f0914 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -17,7 +17,7 @@ */ #include "ExportPackDialog.h" -#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlamePackExportTask.h" #include "ui/dialogs/CustomMessageBox.h" @@ -29,104 +29,135 @@ #include #include #include -#include "FastFileIconProvider.h" #include "FileSystem.h" #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" -ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) - : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider) +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); - ui->setupUi(this); - ui->name->setPlaceholderText(instance->name()); - ui->name->setText(instance->settings()->get("ExportName").toString()); - ui->version->setText(instance->settings()->get("ExportVersion").toString()); - ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + m_ui->setupUi(this); + m_ui->name->setPlaceholderText(instance->name()); + m_ui->name->setText(instance->settings()->get("ExportName").toString()); + m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); + m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + + connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { setWindowTitle(tr("Export Modrinth Pack")); - ui->authorLabel->hide(); - ui->author->hide(); + m_ui->authorLabel->hide(); + m_ui->author->hide(); + + m_ui->recommendedMemoryWidget->hide(); - ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); + m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); } else { setWindowTitle(tr("Export CurseForge Pack")); - ui->summaryLabel->hide(); - ui->summary->hide(); + m_ui->summaryLabel->hide(); + m_ui->summary->hide(); + + const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt(); + + if (recommendedRAM > 0) { + m_ui->recommendedMemoryCheckBox->setChecked(true); + m_ui->recommendedMemory->setValue(recommendedRAM); + } else { + m_ui->recommendedMemoryCheckBox->setChecked(false); - ui->author->setText(instance->settings()->get("ExportAuthor").toString()); + // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount) + const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12); + m_ui->recommendedMemory->setValue(defaultRecommendation); + } + + m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated // the name and version fields mustn't be empty - connect(ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); - connect(ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(m_ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); + connect(m_ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); // the instance name can technically be empty validate(); QFileSystemModel* model = new QFileSystemModel(this); - model->setIconProvider(&icons); + model->setIconProvider(&m_icons); // use the game root - everything outside cannot be exported - const QDir root(instance->gameRoot()); - proxy = new FileIgnoreProxy(instance->gameRoot(), this); - proxy->ignoreFilesWithPath().insert({ "logs", "crash-reports", ".cache", ".fabric", ".quilt" }); - proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); - proxy->setSourceModel(model); + const QDir instanceRoot(instance->instanceRoot()); + m_proxy = new FileIgnoreProxy(instance->instanceRoot(), this); + auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); + for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { + m_proxy->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); + } + m_proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); + m_proxy->ignoreFilesWithSuffix().append(".pw.toml"); + m_proxy->setSourceModel(model); + m_proxy->loadBlockedPathsFromFile(ignoreFileName()); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); - for (const QString& file : root.entryList(filter)) { - if (!(file == "mods" || file == "coremods" || file == "datapacks" || file == "config" || file == "options.txt" || - file == "servers.dat")) - proxy->blockedPaths().insert(file); - } + for (auto resourceModel : instance->resourceLists()) { + if (resourceModel == nullptr) { + continue; + } + + if (!resourceModel->indexDir().exists()) { + continue; + } + + if (resourceModel->dir() == resourceModel->indexDir()) { + continue; + } - MinecraftInstance* mcInstance = dynamic_cast(instance.get()); - if (mcInstance) { - const QDir index = mcInstance->loaderModList()->indexDir(); - if (index.exists()) - proxy->ignoreFilesWithPath().insert(root.relativeFilePath(index.absolutePath())); + m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); } - ui->files->setModel(proxy); - ui->files->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); - ui->files->sortByColumn(0, Qt::AscendingOrder); + m_ui->files->setModel(m_proxy); + m_ui->files->setRootIndex(m_proxy->mapFromSource(model->index(instance->gameRoot()))); + m_ui->files->sortByColumn(0, Qt::AscendingOrder); model->setFilter(filter); model->setRootPath(instance->gameRoot()); - QHeaderView* headerView = ui->files->header(); + QHeaderView* headerView = m_ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); - ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportPackDialog::~ExportPackDialog() { - delete ui; + delete m_ui; } void ExportPackDialog::done(int result) { - auto settings = instance->settings(); - settings->set("ExportName", ui->name->text()); - settings->set("ExportVersion", ui->version->text()); - settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); + m_proxy->saveBlockedPathsToFile(ignoreFileName()); + auto settings = m_instance->settings(); + settings->set("ExportName", m_ui->name->text()); + settings->set("ExportVersion", m_ui->version->text()); + settings->set("ExportOptionalFiles", m_ui->optionalFiles->isChecked()); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - settings->set("ExportSummary", ui->summary->toPlainText()); - else - settings->set("ExportAuthor", ui->author->text()); + settings->set("ExportSummary", m_ui->summary->toPlainText()); + else { + settings->set("ExportAuthor", m_ui->author->text()); + + if (m_ui->recommendedMemoryCheckBox->isChecked()) + settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value()); + else + settings->reset("ExportRecommendedRAM"); + } if (result == Accepted) { - const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); + const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); const QString filename = FS::RemoveInvalidFilenameChars(name); QString output; @@ -148,11 +179,21 @@ void ExportPackDialog::done(int result) Task* task; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { - task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->toPlainText(), ui->optionalFiles->isChecked(), - instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), + m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } else { - task = new FlamePackExportTask(name, ui->version->text(), ui->author->text(), ui->optionalFiles->isChecked(), instance, output, - std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + FlamePackExportOptions options{}; + + options.name = name; + options.version = m_ui->version->text(); + options.author = m_ui->author->text(); + options.optionalFiles = m_ui->optionalFiles->isChecked(); + options.instance = m_instance; + options.output = output; + options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1); + options.recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; + + task = new FlamePackExportTask(std::move(options)); } connect(task, &Task::failed, @@ -174,6 +215,11 @@ void ExportPackDialog::done(int result) void ExportPackDialog::validate() { - ui->buttonBox->button(QDialogButtonBox::Ok) - ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && ui->version->text().isEmpty()); + m_ui->buttonBox->button(QDialogButtonBox::Ok) + ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && m_ui->version->text().isEmpty()); +} + +QString ExportPackDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h index 830c24d25..81e657aa0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.h +++ b/launcher/ui/dialogs/ExportPackDialog.h @@ -22,6 +22,7 @@ #include "BaseInstance.h" #include "FastFileIconProvider.h" #include "FileIgnoreProxy.h" +#include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" namespace Ui { @@ -32,7 +33,7 @@ class ExportPackDialog : public QDialog { Q_OBJECT public: - explicit ExportPackDialog(InstancePtr instance, + explicit ExportPackDialog(MinecraftInstance* instance, QWidget* parent = nullptr, ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); ~ExportPackDialog(); @@ -41,9 +42,12 @@ class ExportPackDialog : public QDialog { void validate(); private: - const InstancePtr instance; - Ui::ExportPackDialog* ui; - FileIgnoreProxy* proxy; - FastFileIconProvider icons; + QString ignoreFileName(); + + private: + MinecraftInstance* m_instance; + Ui::ExportPackDialog* m_ui; + FileIgnoreProxy* m_proxy; + FastFileIconProvider m_icons; const ModPlatform::ResourceProvider m_provider; }; diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index a4a174212..bda8b8dd0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -19,36 +19,56 @@ &Description - - - - - &Name - - - name - - - - - - + - - - &Version + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - version - - - - - - - 1.0.0 - - + + + + &Name: + + + name + + + + + + + + + + &Version: + + + version + + + + + + + 1.0.0 + + + + + + + &Author: + + + author + + + + + + + @@ -62,24 +82,29 @@ - - true + + + 0 + 0 + - - - - - - &Author + + + 0 + 100 + - - author + + + 16777215 + 100 + + + + true - - - @@ -88,7 +113,70 @@ &Options - + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + &Recommended Memory: + + + + + + + false + + + + 0 + 0 + + + + MiB + + + 8 + + + 32768 + + + 128 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -138,10 +226,6 @@ - name - version - summary - author files optionalFiles diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index c2ba68f7a..e8873f9b4 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -46,7 +46,7 @@ ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWi ui->setupUi(this); enableCustom(false); - connect(ui->formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); + connect(ui->formatComboBox, &QComboBox::currentIndexChanged, this, &ExportToModListDialog::formatChanged); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); diff --git a/launcher/ui/dialogs/ExportToModListDialog.ui b/launcher/ui/dialogs/ExportToModListDialog.ui index 3afda2fa8..ec049d7e7 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.ui +++ b/launcher/ui/dialogs/ExportToModListDialog.ui @@ -79,6 +79,9 @@ 0 + + This text supports the following placeholders: {name} - Mod name {mod_id} - Mod ID {url} - Mod URL {version} - Mod version {authors} - Mod authors + diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index a7b44eab0..b56e95dba 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -15,7 +15,9 @@ #include #include +#include #include +#include #include "Application.h" @@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->setupUi(this); setWindowModality(Qt::WindowModal); + searchBar = new QLineEdit(this); + searchBar->setPlaceholderText(tr("Search...")); + ui->verticalLayout->insertWidget(0, searchBar); + + proxyModel = new QSortFilterProxyModel(this); + proxyModel->setSourceModel(APPLICATION->icons()); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui->iconView->setModel(proxyModel); + auto contentsWidget = ui->iconView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); @@ -47,7 +58,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - contentsWidget->setItemDelegate(new ListViewDelegate()); + contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget)); // contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); @@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->installEventFilter(this); - contentsWidget->setModel(APPLICATION->icons().get()); + contentsWidget->setModel(proxyModel); // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); @@ -66,16 +77,18 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); - connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); + connect(buttonAdd, &QPushButton::clicked, this, &IconPickerDialog::addNewIcon); + connect(buttonRemove, &QPushButton::clicked, this, &IconPickerDialog::removeSelectedIcon); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &IconPickerDialog::activated); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &IconPickerDialog::selectionChanged); auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); + connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); + // Prevent incorrect indices from e.g. filesystem changes + connect(APPLICATION->icons(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) @@ -162,5 +175,10 @@ IconPickerDialog::~IconPickerDialog() void IconPickerDialog::openFolder() { - DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true); + DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true); +} + +void IconPickerDialog::filterIcons(const QString& query) +{ + proxyModel->setFilterFixedString(query); } diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h index 37e53dcce..db1315338 100644 --- a/launcher/ui/dialogs/IconPickerDialog.h +++ b/launcher/ui/dialogs/IconPickerDialog.h @@ -16,6 +16,8 @@ #pragma once #include #include +#include +#include namespace Ui { class IconPickerDialog; @@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog { private: Ui::IconPickerDialog* ui; QPushButton* buttonRemove; + QLineEdit* searchBar; + QSortFilterProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); @@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog { void addNewIcon(); void removeSelectedIcon(); void openFolder(); + void filterIcons(const QString& text); }; diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index e3a1e9a6c..80096ed01 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -8,10 +8,11 @@ #include "InstanceList.h" #include +#include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" -ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent) +ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path) { ui->setupUi(this); @@ -34,16 +35,15 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType contentsWidget->setItemDelegate(new ListViewDelegate()); proxyModel = new InstanceProxyModel(this); - proxyModel->setSourceModel(APPLICATION->instances().get()); + proxyModel->setSourceModel(APPLICATION->instances()); proxyModel->sort(0); contentsWidget->setModel(proxyModel); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &ImportResourceDialog::activated); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ImportResourceDialog::selectionChanged); ui->label->setText( - tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); + tr("Choose the instance you would like to import this %1 to.").arg(ModPlatform::ResourceTypeUtils::getName(m_resource_type))); ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h index bbde1ba7b..d96099661 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.h +++ b/launcher/ui/dialogs/ImportResourceDialog.h @@ -3,7 +3,7 @@ #include #include -#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceProxyModel.h" namespace Ui { @@ -14,13 +14,13 @@ class ImportResourceDialog : public QDialog { Q_OBJECT public: - explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr); + explicit ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent = nullptr); ~ImportResourceDialog() override; QString selectedInstanceKey; private: Ui::ImportResourceDialog* ui; - PackedResourceType m_resource_type; + ModPlatform::ResourceType m_resource_type; QString m_file_path; InstanceProxyModel* proxyModel; diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index 7082125f2..44f6ae442 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"); @@ -53,7 +49,7 @@ class InstallLoaderPage : public VersionSelectWidget, public BasePage { QString id() const override { return uid; } QString displayName() const override { return name; } - QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { @@ -88,15 +84,17 @@ 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); + layout->setContentsMargins(0, 0, 0, 0); container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); + buttonLayout->setContentsMargins(0, 0, 6, 6); auto refreshButton = new QPushButton(tr("&Refresh"), this); connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); @@ -110,7 +108,7 @@ InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, c connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); - layout->addLayout(buttonLayout); + container->addButtons(buttonLayout); setWindowTitle(dialogTitle()); setWindowModality(Qt::WindowModal); @@ -167,4 +165,4 @@ void InstallLoaderDialog::done(int result) QDialog::done(result); } -#include "InstallLoaderDialog.moc" \ No newline at end of file +#include "InstallLoaderDialog.moc" 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 40d1eff1e..e6b53d292 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -43,10 +43,15 @@ #include #include +#include +#include #include +#include #include #include +#include "qrencode.h" + MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { ui->setupUi(this); @@ -60,7 +65,6 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS ui->code->setFont(font); connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); }); - ui->qr->setPixmap(QIcon((":/documents/login-qr.svg")).pixmap(QSize(150, 150))); connect(ui->loginButton, &QPushButton::clicked, this, [this] { if (m_url.isValid()) { if (!DesktopServices::openUrl(m_url)) { @@ -135,34 +139,86 @@ void MSALoginDialog::onTaskFailed(QString reason) void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { ui->stackedWidget2->setCurrentIndex(1); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); ui->loginButton->setToolTip(QString("
    %1
    ").arg(url.toString())); m_url = url; } -void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn) +void paintQR(QPainter& painter, const QSize canvasSize, const QString& data, QColor fg) +{ + const auto* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QRecLevel::QR_ECLEVEL_M, QRencodeMode::QR_MODE_8, 1); + if (!qr) { + qWarning() << "Unable to encode" << data << "as QR code"; + return; + } + + painter.setPen(Qt::NoPen); + painter.setBrush(fg); + + // Make sure the QR code fits in the canvas with some padding + const auto qrSize = qr->width; + const auto canvasWidth = canvasSize.width(); + const auto canvasHeight = canvasSize.height(); + const auto scale = 0.8 * std::min(canvasWidth / qrSize, canvasHeight / qrSize); + + // Find an offset to center it in the canvas + const auto offsetX = (canvasWidth - qrSize * scale) / 2; + const auto offsetY = (canvasHeight - qrSize * scale) / 2; + + for (int y = 0; y < qrSize; y++) { + for (int x = 0; x < qrSize; x++) { + auto shouldFillIn = qr->data[y * qrSize + x] & 1; + if (shouldFillIn) { + QRectF r(offsetX + x * scale, offsetY + y * scale, scale, scale); + painter.drawRects(&r, 1); + } + } + } +} + +void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { ui->stackedWidget->setCurrentIndex(1); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); const auto linkString = QString("%2").arg(url, url); - ui->code->setText(code); - auto isDefaultUrl = url == "https://www.microsoft.com/link"; - ui->qr->setVisible(isDefaultUrl); - if (isDefaultUrl) { - ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code.").arg(linkString)); - } else { - ui->qrMessage->setText(tr("Open %1 and enter the above code.").arg(linkString)); + if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { + url += QString("?otc=%1").arg(code); } + ui->code->setText(code); + + auto size = QSize(150, 150); + QPixmap pixmap(size); + pixmap.fill(Qt::white); + + QPainter painter(&pixmap); + paintQR(painter, size, url, Qt::black); + + // Set the generated pixmap to the label + ui->qr->setPixmap(pixmap); + + ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString)); } void MSALoginDialog::onDeviceFlowStatus(QString status) { ui->stackedWidget->setCurrentIndex(0); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); ui->status->setText(status); } void MSALoginDialog::onAuthFlowStatus(QString status) { ui->stackedWidget2->setCurrentIndex(0); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); ui->status2->setText(status); } @@ -174,4 +230,4 @@ MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) return dlg.m_account; } return nullptr; -} \ No newline at end of file +} diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 375ccc57a..f19abbe6d 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -16,7 +16,6 @@ #pragma once #include -#include #include #include "minecraft/auth/AuthFlow.h" diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h deleted file mode 100644 index de5ab46a5..000000000 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "BaseInstance.h" -#include "ResourceDownloadTask.h" -#include "ReviewMessageBox.h" - -#include "minecraft/mod/ModFolderModel.h" - -#include "modplatform/CheckUpdateTask.h" - -class Mod; -class ModrinthCheckUpdate; -class FlameCheckUpdate; -class ConcurrentTask; - -class ModUpdateDialog final : public ReviewMessageBox { - Q_OBJECT - public: - explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, std::shared_ptr mod_model, QList& search_for); - explicit ModUpdateDialog(QWidget* parent, - BaseInstance* instance, - std::shared_ptr mod_model, - QList& search_for, - bool includeDeps); - - void checkCandidates(); - - void appendMod(const CheckUpdateTask::UpdatableMod& info, QStringList requiredBy = {}); - - const QList getTasks(); - auto indexDir() const -> QDir { return m_mod_model->indexDir(); } - - auto noUpdates() const -> bool { return m_no_updates; }; - auto aborted() const -> bool { return m_aborted; }; - - private: - auto ensureMetadata() -> bool; - - private slots: - void onMetadataEnsured(Mod*); - void onMetadataFailed(Mod*, - bool try_others = false, - ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH); - - private: - QWidget* m_parent; - - shared_qobject_ptr m_modrinth_check_task; - shared_qobject_ptr m_flame_check_task; - - const std::shared_ptr m_mod_model; - - QList& m_candidates; - QList m_modrinth_to_update; - QList m_flame_to_update; - - ConcurrentTask::Ptr m_second_try_metadata; - QList> m_failed_metadata; - QList> m_failed_check_update; - - QHash m_tasks; - BaseInstance* m_instance; - - bool m_no_updates = false; - bool m_aborted = false; - bool m_include_deps = false; -}; diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index b5f8ff889..d1e420864 100644 --- a/launcher/ui/dialogs/NewComponentDialog.cpp +++ b/launcher/ui/dialogs/NewComponentDialog.cpp @@ -83,7 +83,8 @@ NewComponentDialog::~NewComponentDialog() void NewComponentDialog::updateDialogState() { auto protoUid = ui->nameTextBox->text().toLower(); - protoUid.remove(QRegularExpression("[^a-z]")); + static const QRegularExpression s_removeChars("[^a-z]"); + protoUid.remove(s_removeChars); if (protoUid.isEmpty()) { ui->uidTextBox->setPlaceholderText(originalPlaceholderText); } else { diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index d9ea0aafb..8d80966ac 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -74,7 +74,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, { ui->setupUi(this); - setWindowIcon(APPLICATION->getThemedIcon("new")); + setWindowIcon(QIcon::fromTheme("new")); InstIconKey = "default"; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); @@ -95,6 +95,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_container = new PageContainer(this, {}, this); + m_container->useSidebarStyle(false); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); m_container->layout()->setContentsMargins(0, 0, 0, 0); ui->verticalLayout->insertWidget(2, m_container); @@ -134,13 +135,9 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, updateDialogState(); if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toString().toUtf8())); } else { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto screen = parent->screen(); -#else - auto screen = QGuiApplication::primaryScreen(); -#endif auto geometry = screen->availableSize(); resize(width(), qMin(geometry.height() - 50, 710)); } @@ -150,7 +147,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, void NewInstanceDialog::reject() { - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); // This is just so that the pages get the close() call and can react to it, if needed. m_container->prepareToClose(); @@ -160,7 +157,7 @@ void NewInstanceDialog::reject() void NewInstanceDialog::accept() { - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); importIconNow(); // This is just so that the pages get the close() call and can react to it, if needed. @@ -320,7 +317,7 @@ void NewInstanceDialog::importIconNow() InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.')); importIcon = false; } - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); } void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) diff --git a/launcher/ui/dialogs/OfflineLoginDialog.cpp b/launcher/ui/dialogs/OfflineLoginDialog.cpp deleted file mode 100644 index d8fbc04fd..000000000 --- a/launcher/ui/dialogs/OfflineLoginDialog.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "OfflineLoginDialog.h" -#include "ui_OfflineLoginDialog.h" - -#include - -OfflineLoginDialog::OfflineLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::OfflineLoginDialog) -{ - ui->setupUi(this); - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - - connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -OfflineLoginDialog::~OfflineLoginDialog() -{ - delete ui; -} - -// Stage 1: User interaction -void OfflineLoginDialog::accept() -{ - setUserInputsEnabled(false); - ui->progressBar->setVisible(true); - - // Setup the login task and start it - m_account = MinecraftAccount::createOffline(ui->userTextBox->text()); - m_loginTask = m_account->login(); - connect(m_loginTask.get(), &Task::failed, this, &OfflineLoginDialog::onTaskFailed); - connect(m_loginTask.get(), &Task::succeeded, this, &OfflineLoginDialog::onTaskSucceeded); - connect(m_loginTask.get(), &Task::status, this, &OfflineLoginDialog::onTaskStatus); - connect(m_loginTask.get(), &Task::progress, this, &OfflineLoginDialog::onTaskProgress); - m_loginTask->start(); -} - -void OfflineLoginDialog::setUserInputsEnabled(bool enable) -{ - ui->userTextBox->setEnabled(enable); - ui->buttonBox->setEnabled(enable); -} - -void OfflineLoginDialog::on_allowLongUsernames_stateChanged(int value) -{ - if (value == Qt::Checked) { - ui->userTextBox->setMaxLength(INT_MAX); - } else { - ui->userTextBox->setMaxLength(16); - } -} - -// Enable the OK button only when the textbox contains something. -void OfflineLoginDialog::on_userTextBox_textEdited(const QString& newText) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty()); -} - -void OfflineLoginDialog::onTaskFailed(const QString& reason) -{ - // Set message - auto lines = reason.split('\n'); - QString processed; - for (auto line : lines) { - if (line.size()) { - processed += "" + line + "
    "; - } else { - processed += "
    "; - } - } - ui->label->setText(processed); - - // Re-enable user-interaction - setUserInputsEnabled(true); - ui->progressBar->setVisible(false); -} - -void OfflineLoginDialog::onTaskSucceeded() -{ - QDialog::accept(); -} - -void OfflineLoginDialog::onTaskStatus(const QString& status) -{ - ui->label->setText(status); -} - -void OfflineLoginDialog::onTaskProgress(qint64 current, qint64 total) -{ - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(current); -} - -// Public interface -MinecraftAccountPtr OfflineLoginDialog::newAccount(QWidget* parent, QString msg) -{ - OfflineLoginDialog dlg(parent); - dlg.ui->label->setText(msg); - if (dlg.exec() == QDialog::Accepted) { - return dlg.m_account; - } - return nullptr; -} diff --git a/launcher/ui/dialogs/OfflineLoginDialog.h b/launcher/ui/dialogs/OfflineLoginDialog.h deleted file mode 100644 index a50024a6c..000000000 --- a/launcher/ui/dialogs/OfflineLoginDialog.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include - -#include "minecraft/auth/MinecraftAccount.h" -#include "tasks/Task.h" - -namespace Ui { -class OfflineLoginDialog; -} - -class OfflineLoginDialog : public QDialog { - Q_OBJECT - - public: - ~OfflineLoginDialog(); - - static MinecraftAccountPtr newAccount(QWidget* parent, QString message); - - private: - explicit OfflineLoginDialog(QWidget* parent = 0); - - void setUserInputsEnabled(bool enable); - - protected slots: - void accept(); - - void onTaskFailed(const QString& reason); - void onTaskSucceeded(); - void onTaskStatus(const QString& status); - void onTaskProgress(qint64 current, qint64 total); - - void on_userTextBox_textEdited(const QString& newText); - void on_allowLongUsernames_stateChanged(int value); - - private: - Ui::OfflineLoginDialog* ui; - MinecraftAccountPtr m_account; - Task::Ptr m_loginTask; -}; diff --git a/launcher/ui/dialogs/OfflineLoginDialog.ui b/launcher/ui/dialogs/OfflineLoginDialog.ui deleted file mode 100644 index 4633cbe3a..000000000 --- a/launcher/ui/dialogs/OfflineLoginDialog.ui +++ /dev/null @@ -1,80 +0,0 @@ - - - OfflineLoginDialog - - - - 0 - 0 - 400 - 150 - - - - - 0 - 0 - - - - Add Account - - - - - - Message label placeholder. - - - Qt::RichText - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 16 - - - Username - - - - - - - Usernames longer than 16 characters cannot be used for LAN games or offline-mode servers. - - - Allow long usernames - - - - - - - 69 - - - false - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index 95bdf99a9..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, SIGNAL(doubleClicked(QModelIndex)), SLOT(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 a6512784f..8e2f2289c 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -55,13 +55,13 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg ui->setupUi(this); ui->errorLabel->setVisible(false); - goodIcon = APPLICATION->getThemedIcon("status-good"); - yellowIcon = APPLICATION->getThemedIcon("status-yellow"); - badIcon = APPLICATION->getThemedIcon("status-bad"); + goodIcon = QIcon::fromTheme("status-good"); + yellowIcon = QIcon::fromTheme("status-yellow"); + badIcon = QIcon::fromTheme("status-bad"); - QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}"); + static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; - nameEdit->setValidator(new QRegularExpressionValidator(permittedNames)); + nameEdit->setValidator(new QRegularExpressionValidator(s_permittedNames)); nameEdit->setClearButtonEnabled(true); validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); @@ -162,8 +162,8 @@ void ProfileSetupDialog::checkName(const QString& name) 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)); + m_check_task = Net::Download::makeByteArray(url, m_check_response.get()); + m_check_task->addHeaderProxy(std::make_unique(headers)); connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished); @@ -206,8 +206,8 @@ void ProfileSetupDialog::setupProfile(const QString& profileName) { "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)); + m_profile_task = Net::Upload::makeByteArray(url, m_profile_response.get(), payloadTemplate.arg(profileName).toUtf8()); + m_profile_task->addHeaderProxy(std::make_unique(headers)); connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished); @@ -226,14 +226,17 @@ struct MojangError { static MojangError fromJSON(QByteArray data) { MojangError out; - out.error = QString::fromUtf8(data); + out.rawError = QString::fromUtf8(data); auto doc = QJsonDocument::fromJson(data, &out.parseError); - auto object = doc.object(); - out.fullyParsed = true; - out.fullyParsed &= Parsers::getString(object.value("path"), out.path); - out.fullyParsed &= Parsers::getString(object.value("error"), out.error); - out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + out.fullyParsed = false; + if (!out.parseError.error) { + auto object = doc.object(); + out.fullyParsed = true; + out.fullyParsed &= Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + } return out; } @@ -261,7 +264,20 @@ void ProfileSetupDialog::setupProfileFinished() } else { auto parsedError = MojangError::fromJSON(*m_profile_response); ui->errorLabel->setVisible(true); - ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage); + + QString errorMessage = + tr("Network Error: %1\nHTTP Status: %2").arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode())); + + if (parsedError.fullyParsed) { + errorMessage += "Path: " + parsedError.path + "\n"; + errorMessage += "Error: " + parsedError.error + "\n"; + errorMessage += "Message: " + parsedError.errorMessage + "\n"; + } else { + errorMessage += "Failed to parse error from Mojang API: " + parsedError.parseError.errorString() + "\n"; + errorMessage += "Log:\n" + parsedError.rawError + "\n"; + } + + ui->errorLabel->setText(tr("The server responded with the following error:") + "\n\n" + errorMessage); qDebug() << parsedError.rawError; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); button->setEnabled(true); diff --git a/launcher/ui/dialogs/ProfileSetupDialog.h b/launcher/ui/dialogs/ProfileSetupDialog.h index c005a4138..594ec6d51 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.h +++ b/launcher/ui/dialogs/ProfileSetupDialog.h @@ -70,9 +70,9 @@ class ProfileSetupDialog : public QDialog { QTimer checkStartTimer; - std::shared_ptr m_check_response; + std::unique_ptr m_check_response; Net::Download::Ptr m_check_task; - std::shared_ptr m_profile_response; + std::unique_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 9897687e3..aa2f67bdb 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -92,7 +92,7 @@ ProgressDialog::~ProgressDialog() { for (auto conn : this->m_taskConnections) { disconnect(conn); - } + } delete ui; } diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 4a696a49d..50e4418da 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -93,7 +93,7 @@ class ProgressDialog : public QDialog { Ui::ProgressDialog* ui; Task* m_task; - + QList m_taskConnections; bool m_is_multi_step = false; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 06fa6dbf3..5695d160e 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -18,7 +18,6 @@ */ #include "ResourceDownloadDialog.h" -#include #include #include @@ -50,7 +49,7 @@ namespace ResourceDownload { -ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model) +ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, ResourceFolderModel* base_model) : QDialog(parent) , m_base_model(base_model) , m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel) @@ -58,9 +57,11 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share { setObjectName(QStringLiteral("ResourceDownloadDialog")); - resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + resize(static_cast(std::max(0.5 * parent->width(), 400.0)), static_cast(std::max(0.75 * parent->height(), 400.0))); - setWindowIcon(APPLICATION->getThemedIcon("new")); + setWindowIcon(QIcon::fromTheme("new")); + + m_buttons.setContentsMargins(0, 0, 6, 6); // 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 @@ -85,7 +86,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share void ResourceDownloadDialog::accept() { if (!geometrySaveKey().isEmpty()) - APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::accept(); } @@ -106,7 +107,7 @@ void ResourceDownloadDialog::reject() } if (!geometrySaveKey().isEmpty()) - APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::reject(); } @@ -115,6 +116,8 @@ void ResourceDownloadDialog::reject() // won't work with subclasses if we put it in this ctor. void ResourceDownloadDialog::initializeContainer() { + layout()->setContentsMargins(0, 0, 0, 0); + m_container = new PageContainer(this, {}, this); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); m_container->layout()->setContentsMargins(0, 0, 0, 0); @@ -148,10 +151,14 @@ void ResourceDownloadDialog::confirm() QStringList depNames; if (auto task = getModDependenciesTask(); task) { connect(task.get(), &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - connect(task.get(), &Task::succeeded, this, [&]() { - QStringList warnings = task->warnings(); + auto weak = task.toWeakRef(); + connect(task.get(), &Task::succeeded, this, [this, weak]() { + QStringList warnings; + if (auto task = weak.lock()) { + warnings = task->warnings(); + } if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } @@ -182,9 +189,8 @@ void ResourceDownloadDialog::confirm() }); for (auto& task : selected) { auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString()); - confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), - ModPlatform::ProviderCapabilities::name(task->getProvider()), extraInfo.required_by, - task->getVersion().version_type.toString(), !extraInfo.maybe_installed }); + confirm_dialog->appendResource({ task->getName(), task->getFilename(), ModPlatform::ProviderCapabilities::name(task->getProvider()), + extraInfo.required_by, task->getVersion().version_type.toString(), !extraInfo.maybe_installed }); } if (confirm_dialog->exec()) { @@ -263,7 +269,7 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s result->setSearchTerm(prev_page->getSearchTerm()); } -ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) +ModDownloadDialog::ModDownloadDialog(QWidget* parent, ModFolderModel* mods, BaseInstance* instance) : ResourceDownloadDialog(parent, mods), m_instance(instance) { setWindowTitle(dialogTitle()); @@ -272,7 +278,7 @@ ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptrsettings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ModDownloadDialog::getPages() @@ -292,7 +298,7 @@ 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()) { selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); @@ -304,9 +310,7 @@ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() return nullptr; } -ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance) +ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, ResourcePackFolderModel* resource_packs, BaseInstance* instance) : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) { setWindowTitle(dialogTitle()); @@ -315,7 +319,7 @@ ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ResourcePackDownloadDialog::getPages() @@ -329,9 +333,7 @@ QList ResourcePackDownloadDialog::getPages() return pages; } -TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance) +TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, TexturePackFolderModel* resource_packs, BaseInstance* instance) : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) { setWindowTitle(dialogTitle()); @@ -340,7 +342,7 @@ TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList TexturePackDownloadDialog::getPages() @@ -354,9 +356,7 @@ QList TexturePackDownloadDialog::getPages() return pages; } -ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, - const std::shared_ptr& shaders, - BaseInstance* instance) +ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, ShaderPackFolderModel* shaders, BaseInstance* instance) : ResourceDownloadDialog(parent, shaders), m_instance(instance) { setWindowTitle(dialogTitle()); @@ -365,7 +365,7 @@ ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ShaderPackDownloadDialog::getPages() @@ -377,7 +377,7 @@ QList ShaderPackDownloadDialog::getPages() return pages; } -void ModDownloadDialog::setModMetadata(std::shared_ptr meta) +void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr& meta) { switch (meta->provider) { case ModPlatform::ResourceProvider::MODRINTH: @@ -393,4 +393,26 @@ void ModDownloadDialog::setModMetadata(std::shared_ptr meta auto page = selectedPage(); page->openProject(meta->project_id); } + +DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, DataPackFolderModel* data_packs, BaseInstance* instance) + : ResourceDownloadDialog(parent, data_packs), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + 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)); + return pages; +} + } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 7a0d6e895..a85a85a09 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -25,6 +25,7 @@ #include #include "QObjectPtr.h" +#include "minecraft/mod/DataPackFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" @@ -50,13 +51,13 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { public: using DownloadTaskPtr = shared_qobject_ptr; - ResourceDownloadDialog(QWidget* parent, std::shared_ptr base_model); + ResourceDownloadDialog(QWidget* parent, ResourceFolderModel* base_model); void initializeContainer(); void connectButtons(); //: String that gets appended to the download dialog title ("Download " + resourcesString()) - [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); } + virtual QString resourcesString() const { return tr("resources"); } QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; @@ -67,7 +68,9 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { void removeResource(const QString&); const QList getTasks(); - [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } + ResourceFolderModel* getBaseModel() const { return m_base_model; } + + void setResourceMetadata(const std::shared_ptr& meta); public slots: void accept() override; @@ -79,13 +82,13 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { virtual void confirm(); protected: - [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } + virtual QString geometrySaveKey() const { return ""; } void setButtonStatus(); - [[nodiscard]] virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } + virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } protected: - const std::shared_ptr m_base_model; + ResourceFolderModel* m_base_model; PageContainer* m_container = nullptr; @@ -97,18 +100,16 @@ 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); ~ModDownloadDialog() override = default; //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("mods"); } - [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } + QString resourcesString() const override { return tr("mods"); } + QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; - void setModMetadata(std::shared_ptr); - private: BaseInstance* m_instance; }; @@ -117,14 +118,12 @@ class ResourcePackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit ResourcePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance); + explicit ResourcePackDownloadDialog(QWidget* parent, ResourcePackFolderModel* resource_packs, BaseInstance* instance); ~ResourcePackDownloadDialog() override = default; //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("resource packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "RPDownloadGeometry"; } + QString resourcesString() const override { return tr("resource packs"); } + QString geometrySaveKey() const override { return "RPDownloadGeometry"; } QList getPages() override; @@ -136,14 +135,12 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: - explicit TexturePackDownloadDialog(QWidget* parent, - const std::shared_ptr& resource_packs, - BaseInstance* instance); + explicit TexturePackDownloadDialog(QWidget* parent, TexturePackFolderModel* resource_packs, BaseInstance* instance); ~TexturePackDownloadDialog() override = default; //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("texture packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "TPDownloadGeometry"; } + QString resourcesString() const override { return tr("texture packs"); } + QString geometrySaveKey() const override { return "TPDownloadGeometry"; } QList getPages() override; @@ -155,12 +152,29 @@ 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* shader_packs, BaseInstance* instance); ~ShaderPackDownloadDialog() override = default; //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("shader packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } + QString resourcesString() const override { return tr("shader packs"); } + QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } + + QList getPages() override; + + private: + BaseInstance* m_instance; +}; + +class DataPackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit DataPackDownloadDialog(QWidget* parent, DataPackFolderModel* data_packs, BaseInstance* instance); + ~DataPackDownloadDialog() override = default; + + //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) + QString resourcesString() const override { return tr("data packs"); } + QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } QList getPages() override; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp similarity index 60% rename from launcher/ui/dialogs/ModUpdateDialog.cpp rename to launcher/ui/dialogs/ResourceUpdateDialog.cpp index 81f544a90..3587b8c40 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -1,4 +1,4 @@ -#include "ModUpdateDialog.h" +#include "ResourceUpdateDialog.h" #include "Application.h" #include "ChooseProviderDialog.h" #include "CustomMessageBox.h" @@ -22,6 +22,8 @@ #include "modplatform/flame/FlameCheckUpdate.h" #include "modplatform/modrinth/ModrinthCheckUpdate.h" +#include +#include #include #include @@ -32,31 +34,28 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static QList mcLoadersList(BaseInstance* inst) -{ - return static_cast(inst)->getPackProfile()->getModLoadersList(); -} - -ModUpdateDialog::ModUpdateDialog(QWidget* parent, - BaseInstance* instance, - const std::shared_ptr mods, - QList& search_for, - bool includeDeps) - : ReviewMessageBox(parent, tr("Confirm mods to update"), "") +ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + ResourceFolderModel* resourceModel, + QList& searchFor, + bool includeDeps, + QList loadersList) + : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) - , m_mod_model(mods) - , m_candidates(search_for) - , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) + , m_resourceModel(resourceModel) + , m_candidates(searchFor) + , m_secondTryMetadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) - , m_include_deps(includeDeps) + , m_includeDeps(includeDeps) + , m_loadersList(std::move(loadersList)) { ReviewMessageBox::setGeometry(0, 0, 800, 600); - ui->explainLabel->setText(tr("You're about to update the following mods:")); - ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); + ui->explainLabel->setText(tr("You're about to update the following resources:")); + ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!")); } -void ModUpdateDialog::checkCandidates() +void ResourceUpdateDialog::checkCandidates() { // Ensure mods have valid metadata auto went_well = ensureMetadata(); @@ -66,17 +65,17 @@ void ModUpdateDialog::checkCandidates() } // Report failed metadata generation - if (!m_failed_metadata.empty()) { + if (!m_failedMetadata.empty()) { QString text; - for (const auto& failed : m_failed_metadata) { + for (const auto& failed : m_failedMetadata) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); 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 mods:
    " - "Do you wish to proceed without those mods?"), + tr("Could not generate metadata for the following resources:
    " + "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { @@ -87,28 +86,30 @@ void ModUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loadersList = mcLoadersList(m_instance); SequentialTask check_task(tr("Checking for updates")); - if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_mod_model)); - connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); - check_task.addTask(m_modrinth_check_task); + 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 }); + }); + check_task.addTask(m_modrinthCheckTask); } - if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_mod_model)); - connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({ mod, reason, recover_url }); }); - check_task.addTask(m_flame_check_task); + 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(&check_task, &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - connect(&check_task, &Task::succeeded, this, [&]() { + connect(&check_task, &Task::succeeded, this, [this, &check_task]() { QStringList warnings = check_task.warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); @@ -131,33 +132,33 @@ void ModUpdateDialog::checkCandidates() QList> selectedVers; // Add found updates for Modrinth - if (m_modrinth_check_task) { - auto modrinth_updates = m_modrinth_check_task->getUpdatable(); + if (m_modrinthCheckTask) { + auto modrinth_updates = m_modrinthCheckTask->getUpdates(); for (auto& updatable : modrinth_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); - appendMod(updatable); + appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } - selectedVers.append(m_modrinth_check_task->getDependencies()); + selectedVers.append(m_modrinthCheckTask->getDependencies()); } // Add found updated for Flame - if (m_flame_check_task) { - auto flame_updates = m_flame_check_task->getUpdatable(); + if (m_flameCheckTask) { + auto flame_updates = m_flameCheckTask->getUpdates(); for (auto& updatable : flame_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); - appendMod(updatable); + appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } - selectedVers.append(m_flame_check_task->getDependencies()); + selectedVers.append(m_flameCheckTask->getDependencies()); } // Report failed update checking - if (!m_failed_check_update.empty()) { + if (!m_failedCheckUpdate.empty()) { QString text; - for (const auto& failed : m_failed_check_update) { + 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); @@ -175,8 +176,8 @@ void ModUpdateDialog::checkCandidates() } ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), - tr("Could not check or get the following mods for updates:
    " - "Do you wish to proceed without those mods?"), + tr("Could not check or get the following resources for updates:
    " + "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { @@ -186,58 +187,61 @@ void ModUpdateDialog::checkCandidates() } } - if (m_include_deps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - auto depTask = makeShared(m_instance, m_mod_model.get(), selectedVers); - - connect(depTask.get(), &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - - connect(depTask.get(), &Task::succeeded, this, [&]() { - QStringList warnings = depTask->warnings(); - if (warnings.count()) { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies + auto* mod_model = dynamic_cast(m_resourceModel); + + if (mod_model != nullptr) { + auto depTask = makeShared(m_instance, mod_model, selectedVers); + + connect(depTask.get(), &Task::failed, this, [this](const QString& reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }); + auto weak = depTask.toWeakRef(); + connect(depTask.get(), &Task::succeeded, this, [this, weak]() { + QStringList warnings; + if (auto depTask = weak.lock()) { + warnings = depTask->warnings(); + } + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + + ProgressDialog progress_dialog_deps(m_parent); + progress_dialog_deps.setSkipButton(true, tr("Abort")); + progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); + auto dret = progress_dialog_deps.execWithTask(depTask.get()); + + // If the dialog was skipped / some download error happened + if (dret == QDialog::DialogCode::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + static FlameAPI api; + + 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); + 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 + }; + + appendResource(updatable, extraInfo.required_by); + m_tasks.insert(updatable.name, updatable.download); } - }); - - ProgressDialog progress_dialog_deps(m_parent); - progress_dialog_deps.setSkipButton(true, tr("Abort")); - progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); - auto dret = progress_dialog_deps.execWithTask(depTask.get()); - - // If the dialog was skipped / some download error happened - if (dret == QDialog::DialogCode::Rejected) { - m_aborted = true; - QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); - return; - } - static FlameAPI api; - - auto dependencyExtraInfo = depTask->getExtraInfo(); - - for (auto dep : depTask->getDependecies()) { - auto changelog = dep->version.changelog; - if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) - changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); - auto download_task = makeShared(dep->pack, dep->version, m_mod_model); - auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); - CheckUpdateTask::UpdatableMod updatable = { dep->pack->name, - dep->version.hash, - "", - dep->version.version, - dep->version.version_type, - changelog, - dep->pack->provider, - download_task, - !extraInfo.maybe_installed }; - - appendMod(updatable, extraInfo.required_by); - m_tasks.insert(updatable.name, updatable.download); } } - // If there's no mod to be updated + // If there's no resource to be updated if (ui->modTreeWidget->topLevelItemCount() == 0) { - m_no_updates = true; + m_noUpdates = true; } else { // FIXME: Find a more efficient way of doing this! @@ -252,12 +256,12 @@ void ModUpdateDialog::checkCandidates() } } - if (m_aborted || m_no_updates) + if (m_aborted || m_noUpdates) QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); } // Part 1: Ensure we have a valid metadata -auto ModUpdateDialog::ensureMetadata() -> bool +auto ResourceUpdateDialog::ensureMetadata() -> bool { auto index_dir = indexDir(); @@ -265,27 +269,29 @@ auto ModUpdateDialog::ensureMetadata() -> bool // A better use of data structures here could remove the need for this QHash QHash should_try_others; - QList modrinth_tmp; - QList flame_tmp; + QList modrinth_tmp; + QList flame_tmp; bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; - auto addToTmp = [&](Mod* m, ModPlatform::ResourceProvider p) { + // adds resource to list based on provider + auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) { switch (p) { case ModPlatform::ResourceProvider::MODRINTH: - modrinth_tmp.push_back(m); + modrinth_tmp.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - flame_tmp.push_back(m); + flame_tmp.push_back(resource); break; } }; + // ask the user on what provider to seach for the mod first for (auto candidate : m_candidates) { - if (candidate->status() != ModStatus::NoMetadata) { + if (candidate->status() != ResourceStatus::NO_METADATA) { onMetadataEnsured(candidate); continue; } @@ -304,7 +310,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool } ChooseProviderDialog chooser(this); - chooser.setDescription(tr("The mod '%1' does not have a metadata yet. We need to generate it in order to track relevant " + chooser.setDescription(tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant " "information on how to update this mod. " "To do this, please select a mod provider which we can use to check for updates for this mod.") .arg(candidate->name())); @@ -326,10 +332,11 @@ auto ModUpdateDialog::ensureMetadata() -> bool addToTmp(candidate, response.chosen); } + // prepare task for the modrinth mods if (!modrinth_tmp.empty()) { auto modrinth_task = makeShared(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); connect(modrinth_task.get(), &EnsureMetadataTask::failed, @@ -341,10 +348,11 @@ auto ModUpdateDialog::ensureMetadata() -> bool seq.addTask(modrinth_task); } + // prepare task for the flame mods if (!flame_tmp.empty()) { auto flame_task = makeShared(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); - connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); connect(flame_task.get(), &EnsureMetadataTask::failed, @@ -356,8 +364,9 @@ auto ModUpdateDialog::ensureMetadata() -> bool seq.addTask(flame_task); } - seq.addTask(m_second_try_metadata); + 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...")); @@ -366,18 +375,18 @@ auto ModUpdateDialog::ensureMetadata() -> bool return (ret_metadata != QDialog::DialogCode::Rejected); } -void ModUpdateDialog::onMetadataEnsured(Mod* mod) +void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) { // When the mod is a folder, for instance - if (!mod->metadata()) + if (!resource->metadata()) return; - switch (mod->metadata()->provider) { + switch (resource->metadata()->provider) { case ModPlatform::ResourceProvider::MODRINTH: - m_modrinth_to_update.push_back(mod); + m_modrinthToUpdate.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - m_flame_to_update.push_back(mod); + m_flameToUpdate.push_back(resource); break; } } @@ -394,32 +403,32 @@ ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) return ModPlatform::ResourceProvider::FLAME; } -void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::ResourceProvider first_choice) +void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, ModPlatform::ResourceProvider first_choice) { if (try_others) { auto index_dir = indexDir(); - auto task = makeShared(mod, index_dir, next(first_choice)); - connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); - connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); + auto task = makeShared(resource, index_dir, next(first_choice)); + connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); + connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); }); connect(task.get(), &EnsureMetadataTask::failed, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (task->getHashingTask()) { auto seq = makeShared(); seq->addTask(task->getHashingTask()); seq->addTask(task); - m_second_try_metadata->addTask(seq); + m_secondTryMetadata->addTask(seq); } else { - m_second_try_metadata->addTask(task); + m_secondTryMetadata->addTask(task); } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; - m_failed_metadata.append({ mod, reason }); + m_failedMetadata.append({ resource, reason }); } } -void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStringList requiredBy) +void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy) { auto item_top = new QTreeWidgetItem(ui->modTreeWidget); item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); @@ -430,30 +439,34 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri item_top->setExpanded(true); auto provider_item = new QTreeWidgetItem(item_top); - provider_item->setText(0, tr("Provider: %1").arg(ModPlatform::ProviderCapabilities::readableName(info.provider))); + 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 old_version_item = new QTreeWidgetItem(item_top); - old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version)); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version)); + old_version_item->setData(0, Qt::UserRole, info.old_version); 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); if (info.new_version_type.has_value()) { - auto new_version_type_itme = new QTreeWidgetItem(item_top); - new_version_type_itme->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); + 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 (!requiredBy.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(item_top); 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:")); - auto i = 0; for (auto req : requiredBy) { auto reqItem = new QTreeWidgetItem(requiredByItem); reqItem->setText(0, req); - reqItem->insertChildren(i++, { reqItem }); } } @@ -468,13 +481,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri auto changelog_area = new QTextBrowser(); QString text = info.changelog; - switch (info.provider) { - case ModPlatform::ResourceProvider::MODRINTH: { - text = markdownToHTML(info.changelog.toUtf8()); - break; - } - default: - break; + changelog->setData(0, Qt::UserRole, text); + if (info.provider == ModPlatform::ResourceProvider::MODRINTH) { + text = markdownToHTML(info.changelog.toUtf8()); } changelog_area->setHtml(StringUtils::htmlListPatch(text)); @@ -487,7 +496,7 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const QList +auto ResourceUpdateDialog::getTasks() -> const QList { QList list; diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h new file mode 100644 index 000000000..ea81aeb7a --- /dev/null +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -0,0 +1,68 @@ +#pragma once + +#include "BaseInstance.h" +#include "ResourceDownloadTask.h" +#include "ReviewMessageBox.h" + +#include "minecraft/mod/ModFolderModel.h" + +#include "modplatform/CheckUpdateTask.h" + +class Mod; +class ModrinthCheckUpdate; +class FlameCheckUpdate; +class ConcurrentTask; + +class ResourceUpdateDialog final : public ReviewMessageBox { + Q_OBJECT + public: + explicit ResourceUpdateDialog(QWidget* parent, + BaseInstance* instance, + ResourceFolderModel* resourceModel, + QList& searchFor, + bool includeDeps, + QList loadersList = {}); + + void checkCandidates(); + + void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {}); + + const QList getTasks(); + auto indexDir() const -> QDir { return m_resourceModel->indexDir(); } + + auto noUpdates() const -> bool { return m_noUpdates; }; + auto aborted() const -> bool { return m_aborted; }; + + private: + auto ensureMetadata() -> bool; + + private slots: + void onMetadataEnsured(Resource* resource); + void onMetadataFailed(Resource* resource, + bool try_others = false, + ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH); + + private: + QWidget* m_parent; + + shared_qobject_ptr m_modrinthCheckTask; + shared_qobject_ptr m_flameCheckTask; + + ResourceFolderModel* m_resourceModel; + + QList& m_candidates; + QList m_modrinthToUpdate; + QList m_flameToUpdate; + + ConcurrentTask::Ptr m_secondTryMetadata; + QList> m_failedMetadata; + QList> m_failedCheckUpdate; + + QHash m_tasks; + BaseInstance* m_instance; + + bool m_noUpdates = false; + bool m_aborted = false; + bool m_includeDeps = false; + QList m_loadersList; +}; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 96cc8149f..d9556979a 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,9 +1,9 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" -#include "Application.h" - +#include #include +#include ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString const& title, [[maybe_unused]] QString const& icon) : QDialog(parent), ui(new Ui::ReviewMessageBox) @@ -23,6 +23,26 @@ ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString con ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + // Overwrite Ctrl+C functionality to exclude the label when copying text from tree + auto shortcut = new QShortcut(QKeySequence::Copy, ui->modTreeWidget); + connect(shortcut, &QShortcut::activated, [this]() { + auto currentItem = this->ui->modTreeWidget->currentItem(); + if (!currentItem) + return; + auto currentColumn = this->ui->modTreeWidget->currentColumn(); + + auto data = currentItem->data(currentColumn, Qt::UserRole); + QString txt; + + if (data.isValid()) { + txt = data.toString(); + } else { + txt = currentItem->text(currentColumn); + } + + QApplication::clipboard()->setText(txt); + }); } ReviewMessageBox::~ReviewMessageBox() @@ -46,49 +66,32 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) auto filenameItem = new QTreeWidgetItem(itemTop); filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); - - auto childIndx = 0; - itemTop->insertChildren(childIndx++, { filenameItem }); - - if (!info.custom_file_path.isEmpty()) { - auto customPathItem = new QTreeWidgetItem(itemTop); - customPathItem->setText(0, tr("This download will be placed in: %1").arg(info.custom_file_path)); - - itemTop->insertChildren(1, { customPathItem }); - - itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); - itemTop->setToolTip( - childIndx++, - tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); - } + filenameItem->setData(0, Qt::UserRole, info.filename); auto providerItem = new QTreeWidgetItem(itemTop); providerItem->setText(0, tr("Provider: %1").arg(info.provider)); - - itemTop->insertChildren(childIndx++, { providerItem }); + providerItem->setData(0, Qt::UserRole, info.provider); if (!info.required_by.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(itemTop); if (info.required_by.length() == 1) { requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); + requiredByItem->setData(0, Qt::UserRole, info.required_by.back()); } else { requiredByItem->setText(0, tr("Required by:")); - auto i = 0; for (auto req : info.required_by) { auto reqItem = new QTreeWidgetItem(requiredByItem); reqItem->setText(0, req); - reqItem->insertChildren(i++, { reqItem }); } } - itemTop->insertChildren(childIndx++, { requiredByItem }); ui->toggleDepsButton->show(); m_deps << itemTop; } auto versionTypeItem = new QTreeWidgetItem(itemTop); versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); - itemTop->insertChildren(childIndx++, { versionTypeItem }); + versionTypeItem->setData(0, Qt::UserRole, info.version_type); ui->modTreeWidget->addTopLevelItem(itemTop); } @@ -123,4 +126,4 @@ void ReviewMessageBox::on_toggleDepsButton_clicked() auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked; for (auto dep : m_deps) dep->setCheckState(0, state); -}; \ No newline at end of file +}; diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index 82a43bc11..ebc4979a6 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -16,7 +16,6 @@ class ReviewMessageBox : public QDialog { using ResourceInformation = struct res_info { QString name; QString filename; - QString custom_file_path{}; QString provider; QStringList required_by; QString version_type; diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp index 810a1f089..f288fe760 100644 --- a/launcher/ui/dialogs/UpdateAvailableDialog.cpp +++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -22,7 +22,6 @@ #include "UpdateAvailableDialog.h" #include -#include "Application.h" #include "BuildConfig.h" #include "Markdown.h" #include "StringUtils.h" @@ -41,7 +40,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName)); ui->versionAvailableLabel->setText( tr("Version %1 is now available - you have %2 . Would you like to download it now?").arg(availableVersion).arg(currentVersion)); - ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); + ui->icon->setPixmap(QIcon::fromTheme("checkupdate").pixmap(64)); auto releaseNotesHtml = markdownToHTML(releaseNotes); ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index f112e1acf..b700c3a4c 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ */ #include "SkinManageDialog.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" #include "ui_SkinManageDialog.h" #include @@ -52,13 +53,20 @@ #include "ui/instanceview/InstanceDelegate.h" SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) - : QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) + : QDialog(parent), m_acct(acct), m_ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) { - ui->setupUi(this); + m_ui->setupUi(this); + + if (SkinOpenGLWindow::hasOpenGL()) { + m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); + } else { + m_skinPreviewLabel = new QLabel(this); + m_skinPreviewLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } setWindowModality(Qt::WindowModal); - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); @@ -84,32 +92,46 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) contentsWidget->installEventFilter(this); contentsWidget->setModel(&m_list); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &SkinManageDialog::activated); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); - connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SkinManageDialog::selectionChanged); + connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() { + if (m_skinPreview) { + m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); + } + on_capeCombo_currentIndexChanged(0); + }); setupCapes(); - ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); - ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + if (m_skinPreview) { + m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); + } else { + m_ui->skinLayout->insertWidget(0, m_skinPreviewLabel); + } } SkinManageDialog::~SkinManageDialog() { - delete ui; + delete m_ui; + if (m_skinPreview) { + delete m_skinPreview; + } } void SkinManageDialog::activated(QModelIndex index) { - m_selected_skin = index.data(Qt::UserRole).toString(); + m_selectedSkinKey = index.data(Qt::UserRole).toString(); accept(); } -void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected) { if (selected.empty()) return; @@ -117,19 +139,25 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); if (key.isEmpty()) return; - m_selected_skin = key; - auto skin = m_list.skin(key); - if (!skin || !skin->isValid()) + m_selectedSkinKey = key; + auto skin = getSelectedSkin(); + if (!skin) return; - ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); - ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId())); - ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); - ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); + + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } + m_ui->capeCombo->setCurrentIndex(m_capesIdx.value(skin->getCapeId())); + m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); + m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); } void SkinManageDialog::delayed_scroll(QModelIndex model_index) { - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->scrollTo(model_index); } @@ -152,12 +180,22 @@ void SkinManageDialog::on_fileBtn_clicked() } } -QPixmap previewCape(QPixmap capeImage) +QPixmap previewCape(QImage capeImage, bool elytra = false) { - QPixmap preview = QPixmap(10, 16); - QPainter painter(&preview); - painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); - return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation); + if (elytra) { + auto wing = capeImage.copy(34, 2, 12, 20); + QImage mirrored = wing.mirrored(true, false); + + QImage combined(wing.width() * 2 + 1, wing.height() + 14, capeImage.format()); + combined.fill(Qt::transparent); + + QPainter painter(&combined); + painter.drawImage(0, 7, wing); + painter.drawImage(wing.width() + 1, 7, mirrored); + painter.end(); + return QPixmap::fromImage(combined.scaled(84, 128, Qt::KeepAspectRatio, Qt::FastTransformation)); + } + return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } void SkinManageDialog::setupCapes() @@ -165,10 +203,10 @@ void SkinManageDialog::setupCapes() // FIXME: add a model for this, download/refresh the capes on demand auto& accountData = *m_acct->accountData(); int index = 0; - ui->capeCombo->addItem(tr("No Cape"), QVariant()); + m_ui->capeCombo->addItem(tr("No Cape"), QVariant()); auto currentCape = accountData.minecraftProfile.currentCape; if (currentCape.isEmpty()) { - ui->capeCombo->setCurrentIndex(index); + m_ui->capeCombo->setCurrentIndex(index); } auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); @@ -177,9 +215,9 @@ void SkinManageDialog::setupCapes() for (auto& cape : accountData.minecraftProfile.capes) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (cape.data.size()) { - QPixmap capeImage; + QImage capeImage; if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { - m_capes[cape.id] = previewCape(capeImage); + m_capes[cape.id] = capeImage; continue; } } @@ -197,46 +235,63 @@ void SkinManageDialog::setupCapes() } for (auto& cape : accountData.minecraftProfile.capes) { index++; - QPixmap capeImage; + QImage capeImage; if (!m_capes.contains(cape.id)) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (QFileInfo(path).exists() && capeImage.load(path)) { - capeImage = previewCape(capeImage); m_capes[cape.id] = capeImage; } } if (!capeImage.isNull()) { - ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id); } else { - ui->capeCombo->addItem(cape.alias, cape.id); + m_ui->capeCombo->addItem(cape.alias, cape.id); } - m_capes_idx[cape.id] = index; + m_capesIdx[cape.id] = index; } } void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { - auto id = ui->capeCombo->currentData(); + auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - ui->capeImage->setPixmap(cape.scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap( + previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + } else { + m_ui->capeImage->clear(); + } + if (m_skinPreview) { + m_skinPreview->updateCape(cape); } - if (auto skin = m_list.skin(m_selected_skin); skin) { + if (auto skin = getSelectedSkin(); skin) { skin->setCapeId(id.toString()); + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } } } void SkinManageDialog::on_steveBtn_toggled(bool checked) { - if (auto skin = m_list.skin(m_selected_skin); skin) { + if (auto skin = getSelectedSkin(); skin) { skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } } } void SkinManageDialog::accept() { - auto skin = m_list.skin(m_selected_skin); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) { reject(); return; @@ -286,15 +341,15 @@ void SkinManageDialog::on_resetBtn_clicked() void SkinManageDialog::show_context_menu(const QPoint& pos) { QMenu myMenu(tr("Context menu"), this); - myMenu.addAction(ui->action_Rename_Skin); - myMenu.addAction(ui->action_Delete_Skin); + myMenu.addAction(m_ui->action_Rename_Skin); + myMenu.addAction(m_ui->action_Delete_Skin); - myMenu.exec(ui->listView->mapToGlobal(pos)); + myMenu.exec(m_ui->listView->mapToGlobal(pos)); } bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) { - if (obj == ui->listView) { + if (obj == m_ui->listView) { if (ev->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(ev); switch (keyEvent->key()) { @@ -312,24 +367,24 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) return QDialog::eventFilter(obj, ev); } -void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) +void SkinManageDialog::on_action_Rename_Skin_triggered(bool) { - if (!m_selected_skin.isEmpty()) { - ui->listView->edit(ui->listView->currentIndex()); + if (!m_selectedSkinKey.isEmpty()) { + m_ui->listView->edit(m_ui->listView->currentIndex()); } } -void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) +void SkinManageDialog::on_action_Delete_Skin_triggered(bool) { - if (m_selected_skin.isEmpty()) + if (m_selectedSkinKey.isEmpty()) return; - if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) { + if (m_list.getSkinIndex(m_selectedSkinKey) == m_list.getSelectedAccountSkin()) { CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); return; } - auto skin = m_list.skin(m_selected_skin); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) return; @@ -341,15 +396,15 @@ void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) ->exec(); if (response == QMessageBox::Yes) { - if (!m_list.deleteSkin(m_selected_skin, true)) { - m_list.deleteSkin(m_selected_skin, false); + if (!m_list.deleteSkin(m_selectedSkinKey, true)) { + m_list.deleteSkin(m_selectedSkinKey, false); } } } void SkinManageDialog::on_urlBtn_clicked() { - auto url = QUrl(ui->urlLine->text()); + auto url = QUrl(m_ui->urlLine->text()); if (!url.isValid()) { CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); return; @@ -366,13 +421,13 @@ void SkinManageDialog::on_urlBtn_clicked() if (!s.isValid()) { CustomMessageBox::selectable(this, tr("URL is not a valid skin"), QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") - : tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()), + : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()), QMessageBox::Critical) ->show(); QFile::remove(path); return; } - ui->urlLine->setText(""); + m_ui->urlLine->setText(""); if (QFileInfo(path).suffix().isEmpty()) { QFile::rename(path, path + ".png"); } @@ -405,7 +460,7 @@ class WaitTask : public Task { void SkinManageDialog::on_userBtn_clicked() { - auto user = ui->urlLine->text(); + auto user = m_ui->urlLine->text(); if (user.isEmpty()) { return; } @@ -421,8 +476,8 @@ void SkinManageDialog::on_userBtn_clicked() auto uuidLoop = makeShared(); auto profileLoop = makeShared(); - auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut); - auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); + auto getUUID = Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user, uuidOut.get()); + auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut.get()); auto downloadSkin = Net::Download::makeFile(QUrl(), path); QString failReason; @@ -456,7 +511,7 @@ void SkinManageDialog::on_userBtn_clicked() return; } const auto root = doc.object(); - auto id = Json::ensureString(root, "id"); + auto id = root["id"].toString(); if (!id.isEmpty()) { getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); } else { @@ -499,7 +554,7 @@ void SkinManageDialog::on_userBtn_clicked() QFile::remove(path); return; } - ui->urlLine->setText(""); + m_ui->urlLine->setText(""); s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setURL(mcProfile.skin.url); if (m_capes.contains(mcProfile.currentCape)) { @@ -513,14 +568,28 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) QWidget::resizeEvent(event); QSize s = size() * (1. / 3); - if (auto skin = m_list.skin(m_selected_skin); skin) { - if (skin->isValid()) { - ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); - } - } - auto id = ui->capeCombo->currentData(); + auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } else { + m_ui->capeImage->clear(); + } + if (auto skin = getSelectedSkin(); skin && !m_skinPreview) { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } } + +SkinModel* SkinManageDialog::getSelectedSkin() +{ + if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid()) { + return skin; + } + return nullptr; +} + +QHash SkinManageDialog::capes() +{ + return m_capes; +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h index cdb37a513..27bdb93a8 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.h +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,22 +20,27 @@ #include #include +#include #include #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" namespace Ui { class SkinManageDialog; } - -class SkinManageDialog : public QDialog { +class SkinManageDialog : public QDialog, public SkinProvider { Q_OBJECT public: explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); virtual ~SkinManageDialog(); void resizeEvent(QResizeEvent* event) override; + virtual SkinModel* getSelectedSkin() override; + virtual QHash capes() override; + public slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); @@ -56,10 +61,13 @@ class SkinManageDialog : public QDialog { private: void setupCapes(); + private: MinecraftAccountPtr m_acct; - Ui::SkinManageDialog* ui; + Ui::SkinManageDialog* m_ui; SkinList m_list; - QString m_selected_skin; - QHash m_capes; - QHash m_capes_idx; + QString m_selectedSkinKey; + QHash m_capes; + QHash m_capesIdx; + SkinOpenGLWindow* m_skinPreview = nullptr; + QLabel* m_skinPreviewLabel = nullptr; }; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index c77eeaaa3..aeb516854 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -19,17 +19,7 @@ - - - - - - false - - - Qt::AlignCenter - - + @@ -69,6 +59,13 @@ Cape + + + + Preview Elytra + + + diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp new file mode 100644 index 000000000..4404442a4 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "BoxGeometry.h" + +#include +#include +#include +#include + +struct VertexData { + QVector4D position; + QVector2D texCoord; + VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex) {} +}; + +// For cube we would need only 8 vertices but we have to +// duplicate vertex for each face because texture coordinate +// is different. +static const QList vertices = { + // Vertex data for face 0 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3 + // Vertex data for face 1 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7 + + // Vertex data for face 2 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11 + + // Vertex data for face 3 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15 + + // Vertex data for face 4 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19 + + // Vertex data for face 5 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23 +}; + +// Indices for drawing cube faces using triangle strips. +// Triangle strips can be connected by duplicating indices +// between the strips. If connecting strips have opposite +// vertex order then last index of the first strip and first +// index of the second strip needs to be duplicated. If +// connecting strips have same vertex order then only last +// index of the first strip needs to be duplicated. +static const QList indices = { + 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) + 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) + 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) + 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) + 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) + 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) +}; + +static const QList planeVertices = { + { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left + { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right + { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left + { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right +}; +static const QList planeIndices = { + 0, 1, 2, 3, 3 // Face 0 - triangle strip ( v0, v1, v2, v3) +}; + +QList transformVectors(const QMatrix4x4& matrix, const QList& vectors) +{ + QList transformedVectors; + transformedVectors.reserve(vectors.size()); + + for (const QVector4D& vec : vectors) { + if (!matrix.isIdentity()) { + transformedVectors.append(matrix * vec); + } else { + transformedVectors.append(vec); + } + } + + return transformedVectors; +} + +// Function to calculate UV coordinates +// this is pure magic (if something is wrong with textures this is at fault) +QList getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList { + return { + QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight), + QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight), + }; + }; + + auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth); + auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); + auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height); + auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); + auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); + auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); + + auto uvRight = { + right[0], + right[1], + right[3], + right[2], + }; + auto uvLeft = { + left[0], + left[1], + left[3], + left[2], + }; + auto uvTop = { + top[0], + top[1], + top[3], + top[2], + }; + auto uvBottom = { + bottom[3], + bottom[2], + bottom[0], + bottom[1], + }; + auto uvFront = { + front[0], + front[1], + front[3], + front[2], + }; + auto uvBack = { + back[0], + back[1], + back[3], + back[2], + }; + // Create a new array to hold the modified UV data + QList uvData; + uvData.reserve(24); + + // Iterate over the arrays and copy the data to newUVData + for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop }) { + uvData.append(uvArray); + } + + return uvData; +} + +namespace opengl { +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) + : QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) +{ + initializeOpenGLFunctions(); + + // Generate 2 VBOs + m_vertexBuf.create(); + m_indexBuf.create(); +} + +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize) + : BoxGeometry(size, position) +{ + initGeometry(uv.x(), uv.y(), textureDim.x(), textureDim.y(), textureDim.z(), textureSize.width(), textureSize.height()); +} + +BoxGeometry::~BoxGeometry() +{ + m_vertexBuf.destroy(); + m_indexBuf.destroy(); +} + +void BoxGeometry::draw(QOpenGLShaderProgram* program) +{ + // Tell OpenGL which VBOs to use + program->setUniformValue("model_matrix", m_matrix); + m_vertexBuf.bind(); + m_indexBuf.bind(); + + // Offset for position + quintptr offset = 0; + + // Tell OpenGL programmable pipeline how to locate vertex position data + int vertexLocation = program->attributeLocation("a_position"); + program->enableAttributeArray(vertexLocation); + program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData)); + + // Offset for texture coordinate + offset += sizeof(QVector4D); + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + int texcoordLocation = program->attributeLocation("a_texcoord"); + program->enableAttributeArray(texcoordLocation); + program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr); +} + +void BoxGeometry::initGeometry(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight); + + // this should not be needed to be done on each render for most of the objects + QMatrix4x4 transformation; + transformation.translate(m_position); + transformation.scale(m_size); + auto positions = transformVectors(transformation, vertices); + + QList verticesData; + verticesData.reserve(positions.size()); // Reserve space for efficiency + + for (int i = 0; i < positions.size(); ++i) { + verticesData.append(VertexData(positions[i], textureCord[i])); + } + + // Transfer vertex data to VBO 0 + m_vertexBuf.bind(); + m_vertexBuf.allocate(verticesData.constData(), verticesData.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + m_indexBuf.bind(); + m_indexBuf.allocate(indices.constData(), indices.size() * sizeof(GLushort)); + m_indecesCount = indices.size(); +} + +void BoxGeometry::rotate(float angle, const QVector3D& vector) +{ + m_matrix.rotate(angle, vector); +} + +BoxGeometry* BoxGeometry::Plane() +{ + auto b = new BoxGeometry(QVector3D(), QVector3D()); + + // Transfer vertex data to VBO 0 + b->m_vertexBuf.bind(); + b->m_vertexBuf.allocate(planeVertices.constData(), planeVertices.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + b->m_indexBuf.bind(); + b->m_indexBuf.allocate(planeIndices.constData(), planeIndices.size() * sizeof(GLushort)); + b->m_indecesCount = planeIndices.size(); + + return b; +} + +void BoxGeometry::scale(const QVector3D& vector) +{ + m_matrix.scale(vector); +} +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/launcher/ui/dialogs/skins/draw/BoxGeometry.h new file mode 100644 index 000000000..fa1a4c622 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace opengl { +class BoxGeometry : protected QOpenGLFunctions { + public: + BoxGeometry(QVector3D size, QVector3D position); + BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize = { 64, 64 }); + static BoxGeometry* Plane(); + virtual ~BoxGeometry(); + + void draw(QOpenGLShaderProgram* program); + + void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); + void rotate(float angle, const QVector3D& vector); + void scale(const QVector3D& vector); + + private: + QOpenGLBuffer m_vertexBuf; + QOpenGLBuffer m_indexBuf; + QVector3D m_size; + QVector3D m_position; + QMatrix4x4 m_matrix; + GLsizei m_indecesCount; +}; +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp new file mode 100644 index 000000000..7e84a3a49 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -0,0 +1,186 @@ + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui/dialogs/skins/draw/Scene.h" + +#include +#include +#include +#include + +namespace opengl { +Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctions(), m_slim(slim), m_capeVisible(!cape.isNull()) +{ + initializeOpenGLFunctions(); + m_staticComponents = { + // head + new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), + // 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)), + // left leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), + }; + + m_staticComponentsOverlay = { + // head + new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), + // 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)), + // 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)), + }; + + m_normalArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), + }; + + m_normalArmsOverlay = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(6, -6, 0), QPoint(48, 48), QVector3D(4, 12, 4)), + }; + + m_slimArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), + }; + + m_slimArmsOverlay = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(5.5, -6, 0), QPoint(48, 48), QVector3D(3, 12, 4)), + }; + + m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), QVector3D(0, -8, 2.5), QPoint(0, 0), QVector3D(10, 16, 1), QSize(64, 32)); + m_cape->rotate(10.8, QVector3D(1, 0, 0)); + m_cape->rotate(180, QVector3D(0, 1, 0)); + + auto leftWing = + new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); + leftWing->rotate(15, QVector3D(1, 0, 0)); + leftWing->rotate(15, QVector3D(0, 0, 1)); + leftWing->rotate(1, QVector3D(1, 0, 0)); + auto rightWing = + new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); + rightWing->scale(QVector3D(-1, 1, 1)); + rightWing->rotate(15, QVector3D(1, 0, 0)); + rightWing->rotate(15, QVector3D(0, 0, 1)); + rightWing->rotate(1, QVector3D(1, 0, 0)); + m_elytra << leftWing << rightWing; + + // texture init + m_skinTexture = new QOpenGLTexture(skin.mirrored()); + m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest); + + m_capeTexture = new QOpenGLTexture(cape.mirrored()); + m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} +Scene::~Scene() +{ + for (auto array : + { m_staticComponents, m_normalArms, m_slimArms, m_elytra, m_staticComponentsOverlay, m_normalArmsOverlay, m_slimArmsOverlay }) { + for (auto g : array) { + delete g; + } + } + delete m_cape; + + m_skinTexture->destroy(); + delete m_skinTexture; + + m_capeTexture->destroy(); + delete m_capeTexture; +} + +void Scene::draw(QOpenGLShaderProgram* program) +{ + m_skinTexture->bind(); + program->setUniformValue("texture", 0); + for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms, m_staticComponentsOverlay, + m_slim ? m_slimArmsOverlay : m_normalArmsOverlay }) { + for (auto g : toDraw) { + g->draw(program); + } + } + m_skinTexture->release(); + if (m_capeVisible) { + m_capeTexture->bind(); + program->setUniformValue("texture", 0); + 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(); + } +} + +void updateTexture(QOpenGLTexture* texture, const QImage& img) +{ + if (texture) { + if (texture->isBound()) + texture->release(); + texture->destroy(); + texture->create(); + texture->setSize(img.width(), img.height()); + texture->setData(img); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + } +} + +void Scene::setSkin(const QImage& skin) +{ + updateTexture(m_skinTexture, skin.mirrored()); +} + +void Scene::setMode(bool slim) +{ + m_slim = slim; +} +void Scene::setCape(const QImage& cape) +{ + updateTexture(m_capeTexture, cape.mirrored()); +} +void Scene::setCapeVisible(bool visible) +{ + m_capeVisible = visible; +} +void Scene::setElytraVisible(bool elytraVisible) +{ + m_elytraVisible = elytraVisible; +} +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h new file mode 100644 index 000000000..897fbcad9 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "ui/dialogs/skins/draw/BoxGeometry.h" + +#include +namespace opengl { +class Scene : protected QOpenGLFunctions { + public: + Scene(const QImage& skin, bool slim, const QImage& cape); + virtual ~Scene(); + + void draw(QOpenGLShaderProgram* program); + void setSkin(const QImage& skin); + void setCape(const QImage& cape); + void setMode(bool slim); + void setCapeVisible(bool visible); + void setElytraVisible(bool elytraVisible); + + private: + QList m_staticComponents; + QList m_normalArms; + QList m_slimArms; + QList m_staticComponentsOverlay; + QList m_normalArmsOverlay; + QList m_slimArmsOverlay; + BoxGeometry* m_cape = nullptr; + QList m_elytra; + QOpenGLTexture* m_skinTexture = nullptr; + QOpenGLTexture* m_capeTexture = nullptr; + bool m_slim = false; + bool m_capeVisible = false; + bool m_elytraVisible = false; +}; +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp new file mode 100644 index 000000000..e4d1eae8d --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" + +#include +#include +#include +#include +#include +#include + +#include "minecraft/skins/SkinModel.h" +#include "rainbow.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color) + : QOpenGLWindow(), QOpenGLFunctions(), m_baseColor(color), m_parent(parent) +{ + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setDepthBufferSize(24); + setFormat(format); +} + +SkinOpenGLWindow::~SkinOpenGLWindow() +{ + // Make sure the context is current when deleting the texture + // and the buffers. + makeCurrent(); + // double check if resources were initialized because they are not + // initialized together with the object + if (m_scene) { + delete m_scene; + } + if (m_background) { + delete m_background; + } + if (m_backgroundTexture) { + if (m_backgroundTexture->isCreated()) { + m_backgroundTexture->destroy(); + } + delete m_backgroundTexture; + } + if (m_modelProgram) { + if (m_modelProgram->isLinked()) { + m_modelProgram->release(); + } + m_modelProgram->removeAllShaders(); + delete m_modelProgram; + } + if (m_backgroundProgram) { + if (m_backgroundProgram->isLinked()) { + m_backgroundProgram->release(); + } + m_backgroundProgram->removeAllShaders(); + delete m_backgroundProgram; + } + doneCurrent(); +} + +void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) +{ + // Save mouse press position + m_mousePosition = QVector2D(e->pos()); + m_isMousePressed = true; +} + +void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) +{ + // Prevents mouse sticking on Wayland compositors + if (!(event->buttons() & Qt::MouseButton::LeftButton)) { + m_isMousePressed = false; + return; + } + + if (m_isMousePressed) { + int dx = event->position().x() - m_mousePosition.x(); + int dy = event->position().y() - m_mousePosition.y(); + + m_yaw += dx * 0.5f; + m_pitch += dy * 0.5f; + + // Normalize yaw to keep it manageable + if (m_yaw > 360.0f) + m_yaw -= 360.0f; + else if (m_yaw < 0.0f) + m_yaw += 360.0f; + + m_mousePosition = QVector2D(event->pos()); + update(); // Trigger a repaint + } +} + +void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e) +{ + m_isMousePressed = false; +} + +void SkinOpenGLWindow::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0, 0, 1, 1); + + initShaders(); + + generateBackgroundTexture(32, 32, 1); + + QImage skin, cape; + bool slim = false; + if (m_parent) { + if (auto s = m_parent->getSelectedSkin()) { + skin = s->getTexture(); + slim = s->getModel() == SkinModel::SLIM; + cape = m_parent->capes().value(s->getCapeId(), {}); + } + } + + m_scene = new opengl::Scene(skin, slim, cape); + m_background = opengl::BoxGeometry::Plane(); + glEnable(GL_TEXTURE_2D); +} + +void SkinOpenGLWindow::initShaders() +{ + // Skin model shaders + m_modelProgram = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_model.glsl")) + close(); + + // Compile fragment shader + if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_modelProgram->link()) + close(); + + // Bind shader pipeline for use + if (!m_modelProgram->bind()) + close(); + + // Background shaders + m_backgroundProgram = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_background.glsl")) + close(); + + // Compile fragment shader + if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_backgroundProgram->link()) + close(); + + // Bind shader pipeline for use (verification) + if (!m_backgroundProgram->bind()) + close(); +} + +void SkinOpenGLWindow::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + const qreal zNear = 15., fov = 45; + + // Reset projection + m_projection.setToIdentity(); + + // Build the reverse z perspective projection matrix + double radians = qDegreesToRadians(fov / 2.); + double sine = std::sin(radians); + if (sine == 0) + return; + double cotan = std::cos(radians) / sine; + + m_projection(0, 0) = cotan / aspect; + m_projection(1, 1) = cotan; + m_projection(2, 2) = 0.; + m_projection(3, 2) = -1.; + m_projection(2, 3) = zNear; + m_projection(3, 3) = 0.; +} + +void SkinOpenGLWindow::paintGL() +{ + // Adjust the viewport to account for fractional scaling + qreal dpr = devicePixelRatio(); + if (dpr != 1.f) { + QSize scaledSize = size() * dpr; + glViewport(0, 0, scaledSize.width(), scaledSize.height()); + } + + // Clear color and depth buffer + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth buffer + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Enable back face culling + glEnable(GL_CULL_FACE); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_backgroundProgram->bind(); + renderBackground(); + m_backgroundProgram->release(); + + // Calculate model view transformation + QMatrix4x4 matrix; + float yawRad = qDegreesToRadians(m_yaw); + float pitchRad = qDegreesToRadians(m_pitch); + matrix.lookAt(QVector3D( // + m_distance * qCos(pitchRad) * qCos(yawRad), // + m_distance * qSin(pitchRad) - 8, // + m_distance * qCos(pitchRad) * qSin(yawRad)), + QVector3D(0, -8, 0), QVector3D(0, 1, 0)); + + // Set modelview-projection matrix + m_modelProgram->bind(); + m_modelProgram->setUniformValue("mvp_matrix", m_projection * matrix); + + m_scene->draw(m_modelProgram); + m_modelProgram->release(); + + // Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not negotiated properly on the + // first frame + if (m_isFirstFrame) { + m_isFirstFrame = false; + update(); + } +} + +void SkinOpenGLWindow::updateScene(SkinModel* skin) +{ + if (skin && m_scene) { + m_scene->setMode(skin->getModel() == SkinModel::SLIM); + m_scene->setSkin(skin->getTexture()); + update(); + } +} +void SkinOpenGLWindow::updateCape(const QImage& cape) +{ + if (m_scene) { + m_scene->setCapeVisible(!cape.isNull()); + m_scene->setCape(cape); + update(); + } +} + +QColor calculateContrastingColor(const QColor& color) +{ + auto luma = Rainbow::luma(color); + if (luma < 0.5) { + constexpr float contrast = 0.05; + return Rainbow::lighten(color, contrast); + } else { + constexpr float contrast = 0.2; + return Rainbow::darken(color, contrast); + } +} + +QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) +{ + QImage image(width, height, QImage::Format_RGB888); + bool isDarkBase = Rainbow::luma(baseColor) < 0.5; + float contrast = isDarkBase ? 0.05 : 0.45; + auto contrastFunc = std::bind(isDarkBase ? Rainbow::lighten : Rainbow::darken, std::placeholders::_1, contrast, 1.0); + auto white = contrastFunc(baseColor); + auto black = contrastFunc(calculateContrastingColor(baseColor)); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + bool isWhite = ((x / tileSize) + (y / tileSize)) % 2 == 0; + image.setPixelColor(x, y, isWhite ? white : black); + } + } + return image; +} + +void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize) +{ + m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor)); + m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} + +void SkinOpenGLWindow::renderBackground() +{ + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Disable depth buffer writing + m_backgroundTexture->bind(); + m_backgroundProgram->setUniformValue("texture", 0); + m_background->draw(m_backgroundProgram); + m_backgroundTexture->release(); + glDepthMask(GL_TRUE); // Re-enable depth buffer writing + glEnable(GL_DEPTH_TEST); +} + +void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) +{ + // Adjust distance based on scroll + int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down + m_distance -= delta * 0.01f; // Adjust sensitivity factor + m_distance = qMax(16.f, m_distance); // Clamp distance + update(); // Trigger a repaint +} +void SkinOpenGLWindow::setElytraVisible(bool visible) +{ + if (m_scene) + m_scene->setElytraVisible(visible); +} + +bool SkinOpenGLWindow::hasOpenGL() +{ + QOpenGLContext ctx; + return ctx.create(); +} diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h new file mode 100644 index 000000000..6ddc345cf --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +class SkinProvider { + public: + virtual ~SkinProvider() = default; + virtual SkinModel* getSelectedSkin() = 0; + virtual QHash capes() = 0; +}; +class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { + Q_OBJECT + + public: + SkinOpenGLWindow(SkinProvider* parent, QColor color); + virtual ~SkinOpenGLWindow(); + + void updateScene(SkinModel* skin); + void updateCape(const QImage& cape); + void setElytraVisible(bool visible); + + static bool hasOpenGL(); + + protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void initShaders(); + + void generateBackgroundTexture(int width, int height, int tileSize); + void renderBackground(); + + private: + QOpenGLShaderProgram* m_modelProgram; + QOpenGLShaderProgram* m_backgroundProgram; + opengl::Scene* m_scene = nullptr; + + QMatrix4x4 m_projection; + + QVector2D m_mousePosition; + + bool m_isMousePressed = false; + float m_distance = 48; + float m_yaw = 90; // Horizontal rotation angle + float m_pitch = 0; // Vertical rotation angle + + bool m_isFirstFrame = true; + + opengl::BoxGeometry* m_background = nullptr; + QOpenGLTexture* m_backgroundTexture = nullptr; + QColor m_baseColor; + SkinProvider* m_parent = nullptr; +}; diff --git a/launcher/ui/instanceview/AccessibleInstanceView.cpp b/launcher/ui/instanceview/AccessibleInstanceView.cpp index c99fe541a..f9c28c59b 100644 --- a/launcher/ui/instanceview/AccessibleInstanceView.cpp +++ b/launcher/ui/instanceview/AccessibleInstanceView.cpp @@ -2,10 +2,6 @@ #include "AccessibleInstanceView_p.h" #include "InstanceView.h" -#include -#include -#include - #ifndef QT_NO_ACCESSIBILITY QAccessibleInterface* groupViewAccessibleFactory(const QString& classname, QObject* object) diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index d947163bc..c7115801e 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -397,6 +397,7 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, // Prevent instance names longer than 128 chars text.truncate(128); if (text.size() != 0) { + emit textChanged(model->data(index).toString(), text); model->setData(index, text); } } diff --git a/launcher/ui/instanceview/InstanceDelegate.h b/launcher/ui/instanceview/InstanceDelegate.h index 69dd32ba7..98ff9a2fc 100644 --- a/launcher/ui/instanceview/InstanceDelegate.h +++ b/launcher/ui/instanceview/InstanceDelegate.h @@ -33,6 +33,9 @@ class ListViewDelegate : public QStyledItemDelegate { void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; + signals: + void textChanged(QString before, QString after) const; + private slots: void editingDone(); }; diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index c677f3951..c09da3a04 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -49,6 +49,7 @@ #include #include "VisualGroup.h" +#include "ui/themes/CatPainter.h" #include "ui/themes/ThemeManager.h" #include @@ -78,6 +79,9 @@ InstanceView::~InstanceView() { qDeleteAll(m_groups); m_groups.clear(); + if (m_cat) { + m_cat->deleteLater(); + } } void InstanceView::setModel(QAbstractItemModel* model) @@ -89,7 +93,7 @@ void InstanceView::setModel(QAbstractItemModel* model) void InstanceView::dataChanged([[maybe_unused]] const QModelIndex& topLeft, [[maybe_unused]] const QModelIndex& bottomRight, - [[maybe_unused]] const QVector& roles) + [[maybe_unused]] const QList& roles) { scheduleDelayedItemsLayout(); } @@ -172,7 +176,7 @@ void InstanceView::updateScrollbar() void InstanceView::updateGeometries() { - geometryCache.clear(); + m_geometryCache.clear(); QMap cats; @@ -186,8 +190,8 @@ void InstanceView::updateGeometries() cat->update(); } else { auto cat = new VisualGroup(groupName, this); - if (fVisibility) { - cat->collapsed = fVisibility(groupName); + if (m_fVisibility) { + cat->collapsed = m_fVisibility(groupName); } cats.insert(groupName, cat); cat->update(); @@ -400,12 +404,8 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event) if (event->button() == Qt::LeftButton) { emit clicked(index); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if (m_pressedAlreadySelected) { option.state |= QStyle::State_Selected; } @@ -422,7 +422,7 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QModelIndex index = indexAt(event->pos()); if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) { - QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(), event->screenPos(), event->button(), + QMouseEvent me(QEvent::MouseButtonPress, event->position(), event->scenePosition(), event->globalPosition(), event->button(), event->buttons(), event->modifiers()); mousePressEvent(&me); return; @@ -431,12 +431,8 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QPersistentModelIndex persistent = index; emit doubleClicked(persistent); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); } @@ -444,11 +440,15 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) void InstanceView::setPaintCat(bool visible) { - m_catVisible = visible; - if (visible) - m_catPixmap.load(APPLICATION->themeManager()->getCatPack()); - else - m_catPixmap = QPixmap(); + if (m_cat) { + disconnect(m_cat, &CatPainter::updateFrame, this, nullptr); + delete m_cat; + m_cat = nullptr; + } + if (visible) { + m_cat = new CatPainter(APPLICATION->themeManager()->getCatPack(), this); + connect(m_cat, &CatPainter::updateFrame, this, [this] { viewport()->update(); }); + } } void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) @@ -457,27 +457,12 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) QPainter painter(this->viewport()); - if (m_catVisible) { - painter.setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); - int widWidth = this->viewport()->width(); - int widHeight = this->viewport()->height(); - if (m_catPixmap.width() < widWidth) - widWidth = m_catPixmap.width(); - if (m_catPixmap.height() < widHeight) - widHeight = m_catPixmap.height(); - auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); - QRect rectOfPixmap = pixmap.rect(); - rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight()); - painter.drawPixmap(rectOfPixmap.topLeft(), pixmap); - painter.setOpacity(1.0); + if (m_cat) { + m_cat->paint(&painter, this->viewport()->rect()); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.widget = this; if (model()->rowCount() == 0) { @@ -610,7 +595,7 @@ void InstanceView::dragEnterEvent(QDragEnterEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -622,7 +607,7 @@ void InstanceView::dragMoveEvent(QDragMoveEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -648,7 +633,7 @@ void InstanceView::dropEvent(QDropEvent* event) if (event->source() == this) { if (event->possibleActions() & Qt::MoveAction) { - std::pair dropPos = rowDropPos(event->pos()); + std::pair dropPos = rowDropPos(event->position().toPoint()); const VisualGroup* group = dropPos.first; auto hitResult = dropPos.second; @@ -657,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(); @@ -723,8 +708,8 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const } int row = index.row(); - if (geometryCache.contains(row)) { - return *geometryCache[row]; + if (m_geometryCache.contains(row)) { + return *m_geometryCache[row]; } const VisualGroup* cat = category(index); @@ -732,18 +717,14 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const int x = pos.first; // int y = pos.second; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif QRect out; out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); out.setSize(itemDelegate()->sizeHint(option, index)); - geometryCache.insert(row, new QRect(out)); + m_geometryCache.insert(row, new QRect(out)); return out; } @@ -784,12 +765,8 @@ QPixmap InstanceView::renderToPixmap(const QModelIndexList& indices, QRect* r) c QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 30be411a8..5d9dbf729 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -41,6 +41,7 @@ #include #include #include "VisualGroup.h" +#include "ui/themes/CatPainter.h" struct InstanceViewRoles { enum { GroupRole = Qt::UserRole, ProgressValueRole, ProgressMaximumRole }; @@ -56,7 +57,7 @@ class InstanceView : public QAbstractItemView { void setModel(QAbstractItemModel* model) override; using visibilityFunction = std::function; - void setSourceOfGroupCollapseStatus(visibilityFunction f) { fVisibility = f; } + void setSourceOfGroupCollapseStatus(visibilityFunction f) { m_fVisibility = f; } /// return geometry rectangle occupied by the specified model item QRect geometryRect(const QModelIndex& index) const; @@ -83,7 +84,7 @@ class InstanceView : public QAbstractItemView { virtual void updateGeometries() override; protected slots: - virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) override; + virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override; virtual void rowsInserted(const QModelIndex& parent, int start, int end) override; virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void modelReset(); @@ -116,7 +117,7 @@ class InstanceView : public QAbstractItemView { friend struct VisualGroup; QList m_groups; - visibilityFunction fVisibility; + visibilityFunction m_fVisibility; // geometry int m_leftMargin = 5; @@ -127,9 +128,8 @@ class InstanceView : public QAbstractItemView { int m_itemWidth = 100; int m_currentItemsPerRow = -1; int m_currentCursorColumn = -1; - mutable QCache geometryCache; - bool m_catVisible = false; - QPixmap m_catPixmap; + mutable QCache m_geometryCache; + CatPainter* m_cat = nullptr; // point where the currently active mouse action started in geometry coordinates QPoint m_pressedPosition; diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 83103c502..4f7a61eb5 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -55,7 +55,7 @@ void VisualGroup::update() auto itemsPerRow = view->itemsPerRow(); int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow)); - rows = QVector(numRows); + rows = QList(numRows); int maxRowHeight = 0; int positionInRow = 0; @@ -73,12 +73,8 @@ void VisualGroup::update() positionInRow = 0; maxRowHeight = 0; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem viewItemOption; view->initViewItemOption(&viewItemOption); -#else - QStyleOptionViewItem viewItemOption = view->viewOptions(); -#endif auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); if (itemHeight > maxRowHeight) { diff --git a/launcher/ui/instanceview/VisualGroup.h b/launcher/ui/instanceview/VisualGroup.h index 8c6f06bcc..7210e0dfc 100644 --- a/launcher/ui/instanceview/VisualGroup.h +++ b/launcher/ui/instanceview/VisualGroup.h @@ -35,10 +35,10 @@ #pragma once +#include #include #include #include -#include class InstanceView; class QPainter; @@ -61,7 +61,7 @@ struct VisualGroup { InstanceView* view = nullptr; QString text; bool collapsed = false; - QVector rows; + QList rows; int firstItemIndex = 0; int m_verticalPosition = 0; diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 5f69b9d46..938befe13 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -97,7 +97,7 @@ class InstallJavaPage : public QWidget, public BasePage { QString id() const override { return uid; } QString displayName() const override { return name; } - QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { @@ -140,9 +140,9 @@ class InstallJavaPage : public QWidget, public BasePage { void recommendedFilterChanged() { if (m_recommend) { - majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter(m_recommended_majors)); + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny(m_recommended_majors)); } else { - majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter()); + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny()); } } @@ -187,11 +187,14 @@ 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); + layout->setContentsMargins(0, 0, 0, 0); container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); + buttonLayout->setContentsMargins(0, 0, 6, 6); + auto refreshLayout = new QHBoxLayout(this); auto refreshButton = new QPushButton(tr("&Refresh"), this); @@ -217,7 +220,7 @@ InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); - layout->addLayout(buttonLayout); + container->addButtons(buttonLayout); setWindowTitle(dialogTitle()); setWindowModality(Qt::WindowModal); @@ -315,6 +318,7 @@ void InstallDialog::done(int result) QString error = QString(tr("Could not determine Java download type!")); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); deletePath(); + return; } #if defined(Q_OS_MACOS) auto seq = makeShared(tr("Install Java")); diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index d211cb4d3..7e52b5f65 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -21,42 +21,63 @@ #include #include "Application.h" -#include "settings/SettingsObject.h" -#include "ui/widgets/IconLabel.h" #include "ui/widgets/PageContainer.h" PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QDialog(parent) { setWindowTitle(pageProvider->dialogTitle()); - m_container = new PageContainer(pageProvider, defaultId, this); + m_container = new PageContainer(pageProvider, std::move(defaultId), this); + + auto* mainLayout = new QVBoxLayout(this); + + auto* focusStealer = new QPushButton(this); + mainLayout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); - QVBoxLayout* mainLayout = new QVBoxLayout; mainLayout->addWidget(m_container); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0, 0, 0, 0); + setLayout(mainLayout); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); - buttons->button(QDialogButtonBox::Close)->setDefault(true); - buttons->button(QDialogButtonBox::Close)->setText(tr("Close")); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setText(tr("&OK")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("&Cancel")); buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); - buttons->setContentsMargins(6, 0, 6, 0); + buttons->setContentsMargins(0, 0, 6, 6); m_container->addButtons(buttons); - connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close())); - connect(buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()), m_container, SLOT(help())); + connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &PageDialog::accept); + connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); + connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toString().toUtf8())); +} + +void PageDialog::accept() +{ + if (handleClose()) + QDialog::accept(); } void PageDialog::closeEvent(QCloseEvent* event) { - qDebug() << "Paged dialog close requested"; - if (m_container->prepareToClose()) { - qDebug() << "Paged dialog close approved"; - APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); - qDebug() << "Paged dialog geometry saved"; + if (handleClose()) QDialog::closeEvent(event); - } +} + +bool PageDialog::handleClose() +{ + qDebug() << "Paged dialog close requested"; + if (!m_container->prepareToClose()) + return false; + + qDebug() << "Paged dialog close approved"; + APPLICATION->settings()->set("PagedGeometry", QString::fromUtf8(saveGeometry().toBase64())); + qDebug() << "Paged dialog geometry saved"; + + emit applied(); + return true; } diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index aa50bc5e1..9a8a3ccaa 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -25,8 +25,13 @@ class PageDialog : public QDialog { explicit PageDialog(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageDialog() {} - private slots: - virtual void closeEvent(QCloseEvent* event); + signals: + void applied(); + + private: + void accept() override; + void closeEvent(QCloseEvent* event) override; + bool handleClose(); private: PageContainer* m_container; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index a137c4cde..ebe93e5b4 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -59,10 +59,9 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, PasteUpload::PasteType::Hastebin }; - static QRegularExpression validUrlRegExp("https?://.+"); - static QRegularExpression validMSAClientID( + static const QRegularExpression s_validUrlRegExp("https?://.+"); + static const QRegularExpression s_validMSAClientID( QRegularExpression::anchoredPattern("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); - static QRegularExpression validFlameKey(QRegularExpression::anchoredPattern("\\$2[ayb]\\$.{56}")); ui->setupUi(this); @@ -75,10 +74,10 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); // NOTE: this allows http://, but we replace that with https later anyway - ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL)); - ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); - ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); - ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); + ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); + ui->resourceURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->resourceURL)); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); + ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); @@ -137,6 +136,8 @@ void APIPage::loadSettings() ui->msaClientID->setText(msaClientID); QString metaURL = s->get("MetaURLOverride").toString(); ui->metaURL->setText(metaURL); + QString resourceURL = s->get("ResourceURL").toString(); + ui->resourceURL->setText(resourceURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); QString modrinthToken = s->get("ModrinthToken").toString(); @@ -156,18 +157,35 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); 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); } + + if (!resourceURL.isEmpty() && !resourceURL.path().endsWith('/')) { + QString path = resourceURL.path(); + path.append('/'); + resourceURL.setPath(path); + } + + 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); }; + // Don't allow HTTP, since meta is basically RCE with all the jar files. - if (!metaURL.isEmpty() && metaURL.scheme() == "http") { + if (isUnsafe(metaURL)) { metaURL.setScheme("https"); } + // Also don't allow HTTP + if (isUnsafe(resourceURL)) { + resourceURL.setScheme("https"); + } + s->set("MetaURLOverride", metaURL.toString()); + s->set("ResourceURL", resourceURL.toString()); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); QString modrinthToken = ui->modrinthToken->text(); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index d4ed92900..7a22aa069 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -39,7 +39,6 @@ #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -53,8 +52,8 @@ class APIPage : public QWidget, public BasePage { explicit APIPage(QWidget* parent = 0); ~APIPage(); - QString displayName() const override { return tr("APIs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("worlds"); } + QString displayName() const override { return tr("Services"); } + QIcon icon() const override { return QIcon::fromTheme("worlds"); } QString id() const override { return "apis"; } QString helpPage() const override { return "APIs"; } virtual bool apply() override; diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 05c256bb2..834eebb96 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -10,7 +10,7 @@ 620 - + 0 @@ -24,15 +24,20 @@ 0 - - - 0 + + + true - - - Services - - + + + + 0 + -216 + 816 + 832 + + + @@ -50,7 +55,14 @@ - + + + + 0 + 0 + + + @@ -65,7 +77,7 @@ - + Use Default true @@ -92,7 +104,7 @@ - + You can set this to a third-party metadata server to use patched libraries or other hacks. @@ -102,19 +114,31 @@ true + + true + - + Use Default + + + + + + + Assets Server + + - + - Enter a custom URL for meta here. + You can set this to another server if you have problems with downloading assets. Qt::RichText @@ -127,39 +151,58 @@ + + + + Use Default + + + - - - Qt::Vertical - - + + - 20 - 40 + 0 + 0 - + + User Agent + + + + + + Use Default + + + + + + + Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. + + + true + + + + + - - - - - API Keys - - - &Microsoft Authentication + &API Keys - + - Note: you probably don't need to set this if logging in via Microsoft Authentication already works. + &Microsoft Authentication Qt::RichText @@ -167,19 +210,25 @@ true + + true + + + msaClientID + - (Default) + Use Default - + - Enter a custom client ID for Microsoft Authentication here. + Note: you probably don't need to set this if logging in via Microsoft Authentication already works. Qt::RichText @@ -187,37 +236,28 @@ true - - true - - - - - - - - true - - - &Modrinth API - - - - - - <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> + + + + Qt::Vertical - - true + + QSizePolicy::Fixed - + + + 0 + 6 + + + - - + + - Enter a custom API token for Modrinth here. + Mod&rinth Qt::RichText @@ -228,41 +268,54 @@ true + + modrinthToken + - + true - (None) + Use None - - - - - - - true - - - &CurseForge Core API - - - - + + - Note: you probably don't need to set this if CurseForge already works. + <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> + + + true + + + true - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + - Enter a custom API Key for CurseForge here. + &CurseForge Qt::RichText @@ -273,90 +326,71 @@ true + + flameKey + - + true - (Default) + Use Default - - - - - - - Technic Client ID - - - + - <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + Note: you probably don't need to set this if CurseForge already works. + + + true - - - (None) + + + Qt::Vertical - + + QSizePolicy::Fixed + + + + 0 + 6 + + + - Enter a custom GUID client ID for Technic here. + &Technic + + + technicClientID - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Miscellaneous - - - - - - - 0 - 0 - - - - User Agent - - - + + + Use Default + + - + - Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. + <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + + + true @@ -364,14 +398,14 @@ - + Qt::Vertical - 20 - 40 + 0 + 0 diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 90eb6becf..65e622bb3 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -44,9 +44,9 @@ #include +#include "ui/dialogs/ChooseOfflineNameDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" -#include "ui/dialogs/OfflineLoginDialog.h" #include "Application.h" @@ -61,7 +61,7 @@ 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); @@ -78,9 +78,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(); @@ -141,10 +141,21 @@ void AccountListPage::on_actionAddMicrosoft_triggered() void AccountListPage::on_actionAddOffline_triggered() { - MinecraftAccountPtr account = - OfflineLoginDialog::newAccount(this, tr("Please enter your desired username to add your offline account.")); + if (!m_accounts->anyAccountIsValid()) { + QMessageBox::warning(this, tr("Error"), + tr("You must add a Microsoft account that owns Minecraft before you can add an offline account." + "

    " + "If you have lost your account you can contact Microsoft for support.")); + return; + } - if (account) { + ChooseOfflineNameDialog dialog(tr("Please enter your desired username to add your offline account."), this); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + if (const MinecraftAccountPtr account = MinecraftAccount::createOffline(dialog.getUsername())) { + account->login()->start(); // The task will complete here. m_accounts->addAccount(account); if (m_accounts->count() == 1) { m_accounts->setDefaultAccount(account); @@ -199,11 +210,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); @@ -217,6 +234,8 @@ void AccountListPage::updateButtonStates() ui->actionNoDefault->setEnabled(true); ui->actionNoDefault->setChecked(false); } + ui->actionMoveUp->setEnabled(accountCanMoveUp); + ui->actionMoveDown->setEnabled(accountCanMoveDown); ui->listView->resizeColumnToContents(3); } @@ -230,3 +249,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 7bd5101c0..bee56cb58 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -41,7 +41,6 @@ #include "ui/pages/BasePage.h" -#include "Application.h" #include "minecraft/auth/AccountList.h" namespace Ui { @@ -59,9 +58,9 @@ class AccountListPage : public QMainWindow, public BasePage { QString displayName() const override { return tr("Accounts"); } QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("accounts"); + auto icon = QIcon::fromTheme("accounts"); if (icon.isNull()) { - icon = APPLICATION->getThemedIcon("noaccount"); + icon = QIcon::fromTheme("noaccount"); } return icon; } @@ -77,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(); @@ -89,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/CustomCommandsPage.h b/launcher/ui/pages/global/AppearancePage.h similarity index 66% rename from launcher/ui/pages/global/CustomCommandsPage.h rename to launcher/ui/pages/global/AppearancePage.h index ec1204ffe..2220db2cd 100644 --- a/launcher/ui/pages/global/CustomCommandsPage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -36,28 +36,31 @@ #pragma once #include -#include - -#include +#include +#include "java/JavaChecker.h" +#include "translations/TranslationsModel.h" #include "ui/pages/BasePage.h" -#include "ui/widgets/CustomCommands.h" +#include "ui/widgets/AppearanceWidget.h" + +class QTextCharFormat; +class SettingsObject; -class CustomCommandsPage : public QWidget, public BasePage { +class AppearancePage : public AppearanceWidget, public BasePage { Q_OBJECT public: - explicit CustomCommandsPage(QWidget* parent = 0); - ~CustomCommandsPage(); - - QString displayName() const override { return tr("Custom Commands"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("custom-commands"); } - QString id() const override { return "custom-commands"; } - QString helpPage() const override { return "Custom-commands"; } - bool apply() override; - void retranslate() override; - - private: - void applySettings(); - void loadSettings(); - CustomCommands* commands; + explicit AppearancePage(QWidget* parent = nullptr) : AppearanceWidget(false, parent) { layout()->setContentsMargins(0, 0, 6, 0); } + + QString displayName() const override { return tr("Appearance"); } + QIcon icon() const override { return QIcon::fromTheme("appearance"); } + QString id() const override { return "appearance-settings"; } + QString helpPage() const override { return "Launcher-settings"; } + + bool apply() override + { + applySettings(); + return true; + } + + void retranslate() override { retranslateUi(); } }; diff --git a/launcher/ui/pages/global/CustomCommandsPage.cpp b/launcher/ui/pages/global/CustomCommandsPage.cpp deleted file mode 100644 index cc8518c2f..000000000 --- a/launcher/ui/pages/global/CustomCommandsPage.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "CustomCommandsPage.h" -#include -#include -#include - -CustomCommandsPage::CustomCommandsPage(QWidget* parent) : QWidget(parent) -{ - auto verticalLayout = new QVBoxLayout(this); - verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - verticalLayout->setContentsMargins(0, 0, 0, 0); - - auto tabWidget = new QTabWidget(this); - tabWidget->setObjectName(QStringLiteral("tabWidget")); - commands = new CustomCommands(this); - commands->setContentsMargins(6, 6, 6, 6); - tabWidget->addTab(commands, "Foo"); - tabWidget->tabBar()->hide(); - verticalLayout->addWidget(tabWidget); - loadSettings(); -} - -CustomCommandsPage::~CustomCommandsPage() {} - -bool CustomCommandsPage::apply() -{ - applySettings(); - return true; -} - -void CustomCommandsPage::applySettings() -{ - auto s = APPLICATION->settings(); - s->set("PreLaunchCommand", commands->prelaunchCommand()); - s->set("WrapperCommand", commands->wrapperCommand()); - s->set("PostExitCommand", commands->postexitCommand()); -} - -void CustomCommandsPage::loadSettings() -{ - auto s = APPLICATION->settings(); - commands->initialize(false, true, s->get("PreLaunchCommand").toString(), s->get("WrapperCommand").toString(), - s->get("PostExitCommand").toString()); -} - -void CustomCommandsPage::retranslate() -{ - commands->retranslate(); -} diff --git a/launcher/ui/pages/global/EnvironmentVariablesPage.cpp b/launcher/ui/pages/global/EnvironmentVariablesPage.cpp deleted file mode 100644 index 2d44ed624..000000000 --- a/launcher/ui/pages/global/EnvironmentVariablesPage.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 TheKodeToad - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include - -#include "EnvironmentVariablesPage.h" - -EnvironmentVariablesPage::EnvironmentVariablesPage(QWidget* parent) : QWidget(parent) -{ - auto verticalLayout = new QVBoxLayout(this); - verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - verticalLayout->setContentsMargins(0, 0, 0, 0); - - auto tabWidget = new QTabWidget(this); - tabWidget->setObjectName(QStringLiteral("tabWidget")); - variables = new EnvironmentVariables(this); - variables->setContentsMargins(6, 6, 6, 6); - tabWidget->addTab(variables, "Foo"); - tabWidget->tabBar()->hide(); - verticalLayout->addWidget(tabWidget); - - variables->initialize(false, false, APPLICATION->settings()->get("Env").toMap()); -} - -QString EnvironmentVariablesPage::displayName() const -{ - return tr("Environment Variables"); -} - -QIcon EnvironmentVariablesPage::icon() const -{ - return APPLICATION->getThemedIcon("environment-variables"); -} - -QString EnvironmentVariablesPage::id() const -{ - return "environment-variables"; -} - -QString EnvironmentVariablesPage::helpPage() const -{ - return "Environment-variables"; -} - -bool EnvironmentVariablesPage::apply() -{ - APPLICATION->settings()->set("Env", variables->value()); - return true; -} - -void EnvironmentVariablesPage::retranslate() -{ - variables->retranslate(); -} diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 33e9c5388..470704d29 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -50,7 +50,6 @@ ExternalToolsPage::ExternalToolsPage(QWidget* parent) : QWidget(parent), ui(new Ui::ExternalToolsPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); ui->jsonEditorTextBox->setClearButtonEnabled(true); @@ -128,13 +127,13 @@ void ExternalToolsPage::on_jvisualvmPathBtn_clicked() QString raw_dir = ui->jvisualvmPathEdit->text(); QString error; do { - raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir); + raw_dir = QFileDialog::getOpenFileName(this, tr("VisualVM Executable"), raw_dir); if (raw_dir.isEmpty()) { break; } QString cooked_dir = FS::NormalizePath(raw_dir); if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error)) { - QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); continue; } else { ui->jvisualvmPathEdit->setText(cooked_dir); @@ -146,9 +145,9 @@ void ExternalToolsPage::on_jvisualvmCheckBtn_clicked() { QString error; if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error)) { - QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); } else { - QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK")); + QMessageBox::information(this, tr("OK"), tr("VisualVM setup seems to be OK")); } } @@ -157,7 +156,7 @@ void ExternalToolsPage::on_mceditPathBtn_clicked() QString raw_dir = ui->mceditPathEdit->text(); QString error; do { -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir); #else raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir); @@ -187,7 +186,7 @@ void ExternalToolsPage::on_mceditCheckBtn_clicked() void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked() { - QString raw_file = QFileDialog::getOpenFileName(this, tr("JSON Editor"), + QString raw_file = QFileDialog::getOpenFileName(this, tr("Text Editor"), ui->jsonEditorTextBox->text().isEmpty() #if defined(Q_OS_LINUX) ? QString("/usr/bin") diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 7248f0c99..702ace557 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -37,7 +37,6 @@ #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -51,12 +50,12 @@ class ExternalToolsPage : public QWidget, public BasePage { explicit ExternalToolsPage(QWidget* parent = 0); ~ExternalToolsPage(); - QString displayName() const override { return tr("External Tools"); } + QString displayName() const override { return tr("Tools"); } QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("externaltools"); + auto icon = QIcon::fromTheme("externaltools"); if (icon.isNull()) { - icon = APPLICATION->getThemedIcon("loadermods"); + icon = QIcon::fromTheme("loadermods"); } return icon; } diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 47c77842a..b094e3693 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -7,7 +7,7 @@ 0 0 673 - 751 + 823 @@ -24,28 +24,43 @@ 0 - - - 0 + + + true - - - Tab 1 - - + + + + 0 + 0 + 669 + 819 + + + - + - J&Profiler + &Editors - + - + + + &Text Editor + + + jsonEditorTextBox + + + + + - + - + Browse @@ -54,35 +69,45 @@ - + - Check + Used to edit component JSON files. - + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + - <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">https://www.ej-technologies.com/products/jprofiler/overview.html</a></p></body></html> + &MCEdit + + + mceditPathEdit - - - - - - - J&VisualVM - - - + - + - + Browse @@ -91,16 +116,22 @@ - + + + + 0 + 0 + + Check - + - <html><head/><body><p><a href="https://visualvm.github.io/">https://visualvm.github.io/</a></p></body></html> + <html><head/><body><p><a href="https://www.mcedit.net/">MCEdit Website</a> - Used as world editor in the instance Worlds menu.</p></body></html> @@ -108,18 +139,54 @@ - + - &MCEdit + &Profilers - + - + + + Profilers are accessible through the Launch dropdown menu. + + + jsonEditorTextBox + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + J&Profiler + + + jprofilerPathEdit + + + + + - + - + Browse @@ -128,45 +195,82 @@ - + + + + 0 + 0 + + Check - + - <html><head/><body><p><a href="https://www.mcedit.net/">https://www.mcedit.net/</a></p></body></html> + <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">JProfiler Website</a></p></body></html> - - - - - - - External Editors (leave empty for system default) - - - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + - - + + - &Text Editor: + &VisualVM - jsonEditorTextBox + jvisualvmPathEdit - - + + + + + + + + + Browse + + + + + + + + + + 0 + 0 + + + + Check + + + + + - Browse + <html><head/><body><p><a href="https://visualvm.github.io/">VisualVM Website</a></p></body></html> @@ -180,8 +284,8 @@ - 20 - 216 + 0 + 0 diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 0ae296815..6a44c9290 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -69,18 +69,8 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) ui->managedJavaList->selectCurrent(); ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed")); ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!")); - connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { - ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); - if (!ui->autodetectJavaCheckBox->isChecked()) - ui->autodownloadCheckBox->setChecked(false); - }); - } else { - ui->autodownloadCheckBox->setHidden(true); + } else ui->tabWidget->tabBar()->hide(); - } - - loadSettings(); - updateThresholds(); } JavaPage::~JavaPage() @@ -88,115 +78,16 @@ JavaPage::~JavaPage() delete ui; } -bool JavaPage::apply() -{ - applySettings(); - return true; -} - -void JavaPage::applySettings() -{ - auto s = APPLICATION->settings(); - - // Memory - int min = ui->minMemSpinBox->value(); - int max = ui->maxMemSpinBox->value(); - if (min < max) { - s->set("MinMemAlloc", min); - s->set("MaxMemAlloc", max); - } else { - s->set("MinMemAlloc", max); - s->set("MaxMemAlloc", min); - } - s->set("PermGen", ui->permGenSpinBox->value()); - - // Java Settings - s->set("JavaPath", ui->javaPathTextBox->text()); - s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); - s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); - s->set("AutomaticJavaSwitch", ui->autodetectJavaCheckBox->isChecked()); - s->set("AutomaticJavaDownload", ui->autodownloadCheckBox->isChecked()); - JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); -} -void JavaPage::loadSettings() -{ - auto s = APPLICATION->settings(); - // Memory - int min = s->get("MinMemAlloc").toInt(); - int max = s->get("MaxMemAlloc").toInt(); - if (min < max) { - ui->minMemSpinBox->setValue(min); - ui->maxMemSpinBox->setValue(max); - } else { - ui->minMemSpinBox->setValue(max); - ui->maxMemSpinBox->setValue(min); - } - ui->permGenSpinBox->setValue(s->get("PermGen").toInt()); - - // Java Settings - ui->javaPathTextBox->setText(s->get("JavaPath").toString()); - ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); - ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); - ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); - ui->autodetectJavaCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool()); - ui->autodownloadCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool() && s->get("AutomaticJavaDownload").toBool()); -} - -void JavaPage::on_javaDetectBtn_clicked() -{ - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; - } - - JavaInstallPtr java; - - VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); - vselect.setResizeOn(2); - vselect.exec(); - - if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { - java = std::dynamic_pointer_cast(vselect.selectedVersion()); - ui->javaPathTextBox->setText(java->path); - if (!java->is_64bit && APPLICATION->settings()->get("MaxMemAlloc").toInt() > 2048) { - CustomMessageBox::selectable(this, tr("Confirm Selection"), - tr("You selected a 32-bit version of Java.\n" - "This installation does not support more than 2048MiB of RAM.\n" - "Please make sure that the maximum memory value is lower."), - QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) - ->exec(); - } - } -} - -void JavaPage::on_javaBrowseBtn_clicked() +void JavaPage::retranslate() { - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); - - // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (raw_path.isEmpty()) { - return; - } - - QString cooked_path = FS::NormalizePath(raw_path); - QFileInfo javaInfo(cooked_path); - ; - if (!javaInfo.exists() || !javaInfo.isExecutable()) { - return; - } - ui->javaPathTextBox->setText(cooked_path); + ui->retranslateUi(this); } -void JavaPage::on_javaTestBtn_clicked() +bool JavaPage::apply() { - if (checker) { - return; - } - checker.reset(new JavaCommon::TestCheck(this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), - ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); - connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); - checker->run(); + ui->javaSettings->saveSettings(); + JavaCommon::checkJVMArgs(APPLICATION->settings()->get("JvmArgs").toString(), this); + return true; } void JavaPage::on_downloadJavaButton_clicked() @@ -206,51 +97,6 @@ void JavaPage::on_downloadJavaButton_clicked() ui->managedJavaList->loadList(); } -void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) -{ - updateThresholds(); -} - -void JavaPage::checkerFinished() -{ - checker.reset(); -} - -void JavaPage::retranslate() -{ - ui->retranslateUi(this); -} - -void JavaPage::updateThresholds() -{ - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; - unsigned int maxMem = ui->maxMemSpinBox->value(); - unsigned int minMem = ui->minMemSpinBox->value(); - - QString iconName; - - if (maxMem >= sysMiB) { - iconName = "status-bad"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (maxMem > (sysMiB * 0.9)) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (maxMem < minMem) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); - } else { - iconName = "status-good"; - ui->labelMaxMemIcon->setToolTip(""); - } - - { - auto height = ui->labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - ui->labelMaxMemIcon->setPixmap(pix); - } -} - void JavaPage::on_removeJavaButton_clicked() { auto version = ui->managedJavaList->selectedVersion(); diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 0a1c4a6be..79a3d1b96 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -35,12 +35,12 @@ #pragma once -#include #include #include #include #include "JavaCommon.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/JavaSettingsWidget.h" class SettingsObject; @@ -56,29 +56,18 @@ class JavaPage : public QWidget, public BasePage { ~JavaPage(); QString displayName() const override { return tr("Java"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("java"); } + QIcon icon() const override { return QIcon::fromTheme("java"); } QString id() const override { return "java-settings"; } QString helpPage() const override { return "Java-settings"; } - bool apply() override; void retranslate() override; - void updateThresholds(); - - private: - void applySettings(); - void loadSettings(); + bool apply() override; private slots: - void on_javaDetectBtn_clicked(); - void on_javaTestBtn_clicked(); - void on_javaBrowseBtn_clicked(); void on_downloadJavaButton_clicked(); void on_removeJavaButton_clicked(); void on_refreshJavaButton_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - void checkerFinished(); private: Ui::JavaPage* ui; - unique_qobject_ptr checker; }; diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index e6bbeb15a..3ed28cf30 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -24,7 +24,7 @@ 0 - 0 + 6 0 @@ -40,368 +40,93 @@ - - - Memory - - - - - - Ma&ximum memory allocation: - - - maxMemSpinBox - - - - - - - &PermGen: - - - permGenSpinBox - - - - - - - &Minimum memory allocation: - - - minMemSpinBox - - - - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 999999999 - - - 8 - - - 64 - - - - - - - - - - maxMemSpinBox - - - - - - - - - - Java Runtime + + + true - - - - - - 0 - 0 - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - &Skip Java compatibility checks - - - - - - - - - - 0 - 0 - - - - &Auto-detect... - - - - - - - - 0 - 0 - - - - &Test - - - - - - - - - - 0 - 0 - - - - JVM arguments: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - - - Skip Java &Wizard - - - - - - - true - - - - 0 - 0 - - - - - 16777215 - 100 - - - - - - - - Automatically selects the Java version that is compatible with the current Minecraft instance, based on the major version required. - - - Autodetect Java version - - - - - - - - - - 0 - 0 - - - - &Java path: - - - javaPathTextBox - - - - - - - - - - - 0 - 0 - - - - Browse - - - - - - - - - false - - - Automatically downloads and selects the Java version recommended by Mojang. - - - Auto-download Mojang Java - - - - + + + + 0 + 0 + 535 + 612 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - - Management + Installations - - - Downloaded Java Versions - - - - - - - 0 - 0 - - - - - - - - - - Download - - - - - - - Remove - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Refresh - - - - - - - + + + + + Download + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh + + + + - - - Qt::Vertical + + + + 0 + 0 + - - - 20 - 40 - - - + @@ -416,20 +141,13 @@
    ui/widgets/VersionSelectWidget.h
    1
    + + JavaSettingsWidget + QWidget +
    ui/widgets/JavaSettingsWidget.h
    + 1 +
    - - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaPathTextBox - javaBrowseBtn - javaDetectBtn - javaTestBtn - skipCompatibilityCheckbox - skipJavaWizardCheckbox - jvmArgsTextBox - tabWidget -
    diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index af6fc1727..f4be75782 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -37,6 +37,7 @@ #include "LanguagePage.h" #include +#include "Application.h" #include "ui/widgets/LanguageSelectionWidget.h" LanguagePage::LanguagePage(QWidget* parent) : QWidget(parent) diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index ff7ce7ddc..b376e1cf2 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -36,7 +36,6 @@ #pragma once -#include #include #include #include "ui/pages/BasePage.h" @@ -51,7 +50,7 @@ class LanguagePage : public QWidget, public BasePage { virtual ~LanguagePage(); QString displayName() const override { return tr("Language"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("language"); } + QIcon icon() const override { return QIcon::fromTheme("language"); } QString id() const override { return "language-settings"; } QString helpPage() const override { return "Language-settings"; } bool apply() override; diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 8bbed9643..7a0f11c83 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -72,24 +72,14 @@ LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::Launch ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); - defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat()); - - m_languageModel = APPLICATION->translations(); loadSettings(); ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - - connect(ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &LauncherPage::refreshFontPreview); - connect(ui->consoleFont, &QFontComboBox::currentFontChanged, this, &LauncherPage::refreshFontPreview); - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &LauncherPage::refreshFontPreview); - - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } LauncherPage::~LauncherPage() { delete ui; - delete defaultFormat; } bool LauncherPage::apply() @@ -194,9 +184,9 @@ void LauncherPage::on_skinsDirBrowseBtn_clicked() } } -void LauncherPage::on_metadataDisableBtn_clicked() +void LauncherPage::on_metadataEnableBtn_clicked() { - ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); + ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); } void LauncherPage::applySettings() @@ -217,12 +207,6 @@ void LauncherPage::applySettings() s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value()); // Console settings - s->set("ShowConsole", ui->showConsoleCheck->isChecked()); - s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); - s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); - QString consoleFontFamily = ui->consoleFont->currentFont().family(); - s->set("ConsoleFont", consoleFontFamily); - s->set("ConsoleFontSize", ui->fontSizeBox->value()); s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value()); s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked); @@ -235,7 +219,9 @@ void LauncherPage::applySettings() s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("JavaDir", ui->javaDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); + s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); + // Instance auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) { case Sort_LastLaunch: @@ -247,13 +233,18 @@ void LauncherPage::applySettings() break; } - // Cat - s->set("CatOpacity", ui->catOpacitySpinBox->value()); + if (ui->askToRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "AskEverytime"); + } else if (ui->alwaysRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "PhysicalDir"); + } else if (ui->neverRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "MetadataOnly"); + } // Mods - s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); - s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); - s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); + s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); + s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); + s->set("SkipModpackUpdatePrompt", !ui->modpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -264,11 +255,6 @@ void LauncherPage::loadSettings() ui->updateIntervalSpinBox->setValue(APPLICATION->updater()->getUpdateCheckInterval() / 3600); } - // Toolbar/menu bar settings (not applicable if native menu bar is present) - ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar()); -#ifdef Q_OS_MACOS - ui->toolsBox->setVisible(!QMenuBar().isNativeMenuBar()); -#endif ui->preferMenuBarCheckBox->setChecked(s->get("MenuBarInsteadOfToolBar").toBool()); ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); @@ -277,20 +263,6 @@ void LauncherPage::loadSettings() ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt()); // Console settings - ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); - ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool()); - ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool()); - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - QFont consoleFont(fontFamily); - ui->consoleFont->setCurrentFont(consoleFont); - - bool conversionOk = true; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } - ui->fontSizeBox->setValue(fontSize); - refreshFontPreview(); ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt()); ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool()); @@ -302,68 +274,26 @@ void LauncherPage::loadSettings() ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); ui->javaDirTextBox->setText(s->get("JavaDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); + ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool()); + // Instance QString sortMode = s->get("InstSortMode").toString(); - if (sortMode == "LastLaunch") { ui->sortLastLaunchedBtn->setChecked(true); } else { ui->sortByNameBtn->setChecked(true); } - // Cat - ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt()); + QString renamingMode = s->get("InstRenamingMode").toString(); + ui->askToRenameDirBtn->setChecked(renamingMode == "AskEverytime"); + ui->alwaysRenameDirBtn->setChecked(renamingMode == "PhysicalDir"); + ui->neverRenameDirBtn->setChecked(renamingMode == "MetadataOnly"); // Mods - ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); - ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); - ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); - ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool()); -} - -void LauncherPage::refreshFontPreview() -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - int fontSize = ui->fontSizeBox->value(); - QString fontFamily = ui->consoleFont->currentFont().family(); - ui->fontPreview->clear(); - defaultFormat->setFont(QFont(fontFamily, fontSize)); - - auto print = [this, colors](const QString& message, MessageLevel::Enum level) { - QTextCharFormat format(*defaultFormat); - - QColor bg = colors.background.value(level); - QColor fg = colors.foreground.value(level); - - if (bg.isValid()) - format.setBackground(bg); - - if (fg.isValid()) - format.setForeground(fg); - - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(message, format); - workCursor.insertBlock(); - }; - - print(QString("%1 version: %2 (%3)\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), - MessageLevel::Launcher); - - QDate today = QDate::currentDate(); - - if (today.month() == 10 && today.day() == 31) - print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); - else - print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); - - print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); - print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); - print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); - print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); + ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); + ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); + ui->dependenciesEnableBtn->setChecked(!s->get("ModDependenciesDisabled").toBool()); + ui->modpackUpdatePromptBtn->setChecked(!s->get("SkipModpackUpdatePrompt").toBool()); } void LauncherPage::retranslate() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 02f371b04..263bf08bb 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -38,7 +38,6 @@ #include #include -#include #include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" @@ -57,8 +56,8 @@ class LauncherPage : public QWidget, public BasePage { explicit LauncherPage(QWidget* parent = 0); ~LauncherPage(); - QString displayName() const override { return tr("Launcher"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("launcher"); } + QString displayName() const override { return tr("General"); } + QIcon icon() const override { return QIcon::fromTheme("settings"); } QString id() const override { return "launcher-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override; @@ -75,23 +74,8 @@ class LauncherPage : public QWidget, public BasePage { void on_downloadsDirBrowseBtn_clicked(); void on_javaDirBrowseBtn_clicked(); void on_skinsDirBrowseBtn_clicked(); - void on_metadataDisableBtn_clicked(); - - /*! - * Updates the font preview - */ - void refreshFontPreview(); + void on_metadataEnableBtn_clicked(); private: Ui::LauncherPage* ui; - - /*! - * Stores the currently selected update channel. - */ - QString m_currentUpdateChannel; - - // default format for the font preview... - QTextCharFormat* defaultFormat; - - std::shared_ptr m_languageModel; }; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index a46a6fd5d..0debe3f4d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -6,8 +6,8 @@ 0 0 - 511 - 726 + 767 + 796 @@ -16,7 +16,7 @@ 0 - + 0 @@ -30,371 +30,49 @@ 0 - - - + + + Qt::ScrollBarPolicy::ScrollBarAsNeeded - - QTabWidget::TabShape::Rounded + + true - - 0 - - - - Features - - - - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - true - - - - - 0 - 0 - 473 - 770 - - - - - - - Update Settings - - - - - - Check for updates automatically - - - - - - - - - Update interval - - - - - - - Set it to 0 to only check on launch - - - h - - - 0 - - - 99999999 - - - - - - - - - - - - Folders - - - - - - &Downloads: - - - downloadsDirTextBox - - - - - - - Browse - - - - - - - - - - - - - &Skins: - - - skinsDirTextBox - - - - - - - &Icons: - - - iconsDirTextBox - - - - - - - When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - - - Check downloads folder recursively - - - - - - - - - - &Java: - - - javaDirTextBox - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - Browse - - - - - - - Browse - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - Browse - - - - - - - Browse - - - - - - - - - - Mods - - - - - - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - - Disable using metadata for mods - - - - - - - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - - true - - - - - - - Disable the automatic detection, installation, and updating of mod dependencies. - - - Disable automatic mod dependency management - - - - - - - When creating a new modpack instance, do not suggest updating existing instances instead. - - - Skip modpack update prompt - - - - - - - - - - Miscellaneous - - - - - - 1 - - - - - - - Number of concurrent tasks - - - - - - - 1 - - - - - - - Number of concurrent downloads - - - - - - - Number of manual retries - - - - - - - 0 - - - - - - - Seconds to wait until the requests are terminated - - - Timeout for HTTP requests - - - - - - - s - - - - - - - - - - Qt::Orientation::Vertical - - - - 0 - 0 - - - - - - - - - - - - - User Interface - - + + + + 0 + 0 + 746 + 1194 + + + - + true - Instance view sorting mode + User Interface - + + + + + Instance Sorting + + + + + + + By &name + + + sortingModeGroup + + + @@ -406,147 +84,347 @@ - + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 0 + 6 + + + + + + - By &name + Instance Renaming + + + + + + + Ask what to do - sortingModeGroup + renamingBehaviorGroup + + + + Always rename the folder + + + renamingBehaviorGroup + + + + + + + Never rename the folder + + + renamingBehaviorGroup + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 0 + 6 + + + + + + + + The menubar is more friendly for keyboard-driven interaction. + + + &Replace toolbar with menubar + + + - + - Theme + Updater - - - + + + + + + + How Often? + + + + + + + + 0 + 0 + + + + Set to 0 to only check on launch + + + On Launch + + + hours + + + Every + + + 0 + + + 168 + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + + + Check for updates automatically + + - + - Cat + Folders - - - - - Set the cat's opacity. 0% is fully transparent and 100% is fully opaque. + + + + + + + + Browse + + + + - Opacity + &Java: + + + javaDirTextBox - - - - + + + + Browse - - % + + + + + + &Skins: - - 100 + + skinsDirTextBox - - 0 + + + + + + &Mods: + + + modsDirTextBox + + + + + + + Browse + + + + + + + &Downloads: + + + downloadsDirTextBox + + + + + + + + + + + + + I&nstances: + + + instDirTextBox + + + + + + + Browse + + + + + + + Browse - - - Qt::Orientation::Horizontal + + + + + + Browse - - - 40 - 20 - + + + + + + + + + + + + &Icons: - + + iconsDirTextBox + + - - - - 0 - 0 - - + - Tools + Mods and Modpacks - + - + - The menubar is more friendly for keyboard-driven interaction. + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - &Replace toolbar with menubar + Check &subfolders for blocked mods + + + + + + + When enabled, it will move blocked resources instead of copying them. + + + Move blocked mods instead of copying them - - - - - - - Qt::Orientation::Vertical - - - - 0 - 0 - - - - - - - - - Console - - - - - - Console Settings - - - + + + Store version information provided by mod providers (like Modrinth or CurseForge) for mods. + - Show console while the game is &running + Keep track of mod metadata - + - &Automatically close console when the game quits + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> + + + true - + + + Automatically detect, install, and update mod dependencies. + - Show console when the game &crashes + Install dependencies automatically + + + + + + + When creating a new modpack instance, suggest updating an existing instance instead. + + + Suggest to update an existing instance during modpack installation @@ -554,22 +432,34 @@ - + - &History limit + Console - - - + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + 0 + 0 + + - &Stop logging when log overflows + Log History &Limit: + + + lineLimitSpinBox - + - + 0 0 @@ -591,83 +481,167 @@ + + + + &Stop logging when log overflows + + + - - - - 0 - 0 - - + - Console &font + Tasks - - - + + + - + 0 0 - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + 60 + 0 + + + + 0 + + + + + + + + 0 + 0 + - - false + + + 60 + 0 + - - Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse + + 1 - - + + - + 0 0 + + + 60 + 0 + + + + s + + + + + + + Retry Limit: + + + + + + + Concurrent Download Limit: + + + + + + + Seconds to wait until the requests are terminated + + + HTTP Timeout: + - - - 5 + + + + 0 + 0 + - - 16 + + + 60 + 0 + - - 11 + + 1 + + + + Concurrent Task Limit: + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + Qt::Orientation::Vertical + + + + 0 + 0 + + + + - - - ThemeCustomizationWidget - QWidget -
    ui/widgets/ThemeCustomizationWidget.h
    - 1 -
    -
    - tabWidget scrollArea + preferMenuBarCheckBox autoUpdateCheckBox updateIntervalSpinBox instDirTextBox @@ -683,29 +657,21 @@ downloadsDirTextBox downloadsDirBrowseBtn downloadsDirWatchRecursiveCheckBox - metadataDisableBtn - dependenciesDisableBtn - skipModpackUpdatePromptBtn + downloadsDirMoveCheckBox + metadataEnableBtn + dependenciesEnableBtn + modpackUpdatePromptBtn + lineLimitSpinBox + checkStopLogging numberOfConcurrentTasksSpinBox numberOfConcurrentDownloadsSpinBox numberOfManualRetriesSpinBox timeoutSecondsSpinBox - sortLastLaunchedBtn - sortByNameBtn - catOpacitySpinBox - preferMenuBarCheckBox - showConsoleCheck - autoCloseConsoleCheck - showConsoleErrorCheck - lineLimitSpinBox - checkStopLogging - consoleFont - fontSizeBox - fontPreview + diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp deleted file mode 100644 index 3431dcb9c..000000000 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "MinecraftPage.h" -#include "BuildConfig.h" -#include "ui_MinecraftPage.h" - -#include -#include -#include - -#include "Application.h" -#include "settings/SettingsObject.h" - -#ifdef Q_OS_LINUX -#include "MangoHud.h" -#endif - -MinecraftPage::MinecraftPage(QWidget* parent) : QWidget(parent), ui(new Ui::MinecraftPage) -{ - ui->setupUi(this); - connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeGLFWChanged); - connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeOpenALChanged); - loadSettings(); - updateCheckboxStuff(); -} - -MinecraftPage::~MinecraftPage() -{ - delete ui; -} - -bool MinecraftPage::apply() -{ - applySettings(); - return true; -} - -void MinecraftPage::updateCheckboxStuff() -{ - ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); - ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); -} - -void MinecraftPage::on_maximizedCheckBox_clicked(bool checked) -{ - Q_UNUSED(checked); - updateCheckboxStuff(); -} - -void MinecraftPage::onUseNativeGLFWChanged(bool checked) -{ - ui->lineEditGLFWPath->setEnabled(checked); -} - -void MinecraftPage::onUseNativeOpenALChanged(bool checked) -{ - ui->lineEditOpenALPath->setEnabled(checked); -} - -void MinecraftPage::applySettings() -{ - auto s = APPLICATION->settings(); - - // Window Size - s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); - s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); - s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - - // Native library workarounds - s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); - s->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); - s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); - s->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); - - // Peformance related options - s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); - s->set("EnableMangoHud", ui->enableMangoHud->isChecked()); - s->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); - s->set("UseZink", ui->useZink->isChecked()); - - // Game time - s->set("ShowGameTime", ui->showGameTime->isChecked()); - s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); - s->set("RecordGameTime", ui->recordGameTime->isChecked()); - s->set("ShowGameTimeWithoutDays", ui->showGameTimeWithoutDays->isChecked()); - - // Miscellaneous - s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); - s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - - // Legacy settings - s->set("OnlineFixes", ui->onlineFixes->isChecked()); -} - -void MinecraftPage::loadSettings() -{ - auto s = APPLICATION->settings(); - - // Window Size - ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool()); - ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); - ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); - - ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); - ui->lineEditGLFWPath->setText(s->get("CustomGLFWPath").toString()); - ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); -#ifdef Q_OS_LINUX - if (!APPLICATION->m_detectedGLFWPath.isEmpty()) - ui->lineEditGLFWPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedGLFWPath)); -#endif - ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); - ui->lineEditOpenALPath->setText(s->get("CustomOpenALPath").toString()); - ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); -#ifdef Q_OS_LINUX - if (!APPLICATION->m_detectedOpenALPath.isEmpty()) - ui->lineEditOpenALPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedOpenALPath)); -#endif - - ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool()); - ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool()); - ui->useDiscreteGpuCheck->setChecked(s->get("UseDiscreteGpu").toBool()); - ui->useZink->setChecked(s->get("UseZink").toBool()); - -#if !defined(Q_OS_LINUX) - ui->perfomanceGroupBox->setVisible(false); -#endif - - if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { - ui->enableFeralGamemodeCheck->setDisabled(true); - ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); - } - - if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { - ui->enableMangoHud->setDisabled(true); - ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); - } - - ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); - ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); - ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); - ui->showGameTimeWithoutDays->setChecked(s->get("ShowGameTimeWithoutDays").toBool()); - - ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); - ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); - - ui->onlineFixes->setChecked(s->get("OnlineFixes").toBool()); -} - -void MinecraftPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 5facfbb3f..c21d59a6b 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -38,41 +38,26 @@ #include #include -#include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/MinecraftSettingsWidget.h" class SettingsObject; -namespace Ui { -class MinecraftPage; -} - -class MinecraftPage : public QWidget, public BasePage { +class MinecraftPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit MinecraftPage(QWidget* parent = 0); - ~MinecraftPage(); + explicit MinecraftPage(QWidget* parent = nullptr) : MinecraftSettingsWidget(nullptr, parent) {} + ~MinecraftPage() override {} QString displayName() const override { return tr("Minecraft"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("minecraft"); } + QIcon icon() const override { return QIcon::fromTheme("minecraft"); } QString id() const override { return "minecraft-settings"; } QString helpPage() const override { return "Minecraft-settings"; } - bool apply() override; - void retranslate() override; - - private: - void updateCheckboxStuff(); - void applySettings(); - void loadSettings(); - - private slots: - void on_maximizedCheckBox_clicked(bool checked); - - void onUseNativeGLFWChanged(bool checked); - void onUseNativeOpenALChanged(bool checked); - - private: - Ui::MinecraftPage* ui; + bool apply() override + { + saveSettings(); + return true; + } }; diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui deleted file mode 100644 index 7d2741250..000000000 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ /dev/null @@ -1,351 +0,0 @@ - - - MinecraftPage - - - - 0 - 0 - 936 - 541 - - - - - 0 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QTabWidget::Rounded - - - 0 - - - - General - - - - - - Window Size - - - - - - Start Minecraft &maximized - - - - - - - - - Window &height: - - - windowHeightSpinBox - - - - - - - Window &width: - - - windowWidthSpinBox - - - - - - - 1 - - - 65536 - - - 1 - - - 854 - - - - - - - 1 - - - 65536 - - - 480 - - - - - - - - - - - - Game time - - - - - - Show time spent &playing instances - - - - - - - Show time spent playing across &all instances - - - - - - - &Record time spent playing instances - - - - - - - Show time spent playing in hours - - - - - - - - - - Miscellaneous - - - - - - <html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html> - - - &Close the launcher after game window opens - - - - - - - <html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html> - - - &Quit the launcher after game window closes - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - Tweaks - - - - - - Legacy settings - - - - - - <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html> - - - Enable online fixes (experimental) - - - - - - - - - - Native library workarounds - - - - - - Use system installation of &GLFW - - - - - - - &GLFW library path - - - lineEditGLFWPath - - - - - - - Use system installation of &OpenAL - - - - - - - &OpenAL library path - - - lineEditOpenALPath - - - - - - - false - - - - - - - false - - - - - - - - - - Performance - - - - - - <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> - - - Enable Feral GameMode - - - - - - - <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> - - - Enable MangoHud - - - - - - - <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> - - - Use discrete GPU - - - - - - - <html><head/><body><p>Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used.</p></body></html> - - - Use Zink - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - - - - diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 9caffcb37..0629cc648 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -46,11 +46,10 @@ ProxyPage::ProxyPage(QWidget* parent) : QWidget(parent), ui(new Ui::ProxyPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); loadSettings(); updateCheckboxStuff(); - connect(ui->proxyGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &ProxyPage::proxyGroupChanged); + connect(ui->proxyGroup, &QButtonGroup::buttonClicked, this, &ProxyPage::proxyGroupChanged); } ProxyPage::~ProxyPage() diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index 26118f181..8689a5c80 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -40,7 +40,6 @@ #include #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -55,7 +54,7 @@ class ProxyPage : public QWidget, public BasePage { ~ProxyPage(); QString displayName() const override { return tr("Proxy"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("proxy"); } + QIcon icon() const override { return QIcon::fromTheme("proxy"); } QString id() const override { return "proxy-settings"; } QString helpPage() const override { return "Proxy-settings"; } bool apply() override; diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 91ba46b3d..436a90ad1 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -23,184 +23,205 @@ 0 - - 0 - 0 - - - - - - - - - - This only applies to the launcher. Minecraft does not accept proxy settings. - - - Qt::AlignCenter - - - true - - - - - - - Type - - - - - - Uses your system's default proxy settings. - - - &Default - - - proxyGroup - - - - - - - &None - - - proxyGroup - - - - - - - &SOCKS5 - - - proxyGroup - - - - - - - &HTTP - - - proxyGroup - - - - - - - - - - &Address and Port - - - - - - 127.0.0.1 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::PlusMinus - - - 65535 - - - 8080 - - - - - - - - - - Authentication - - - - - - - - - &Username: - - - proxyUserEdit - - - - - - - &Password: - - - proxyPassEdit - - - - - - - QLineEdit::Password - - - - - - - Note: Proxy username and password are stored in plain text inside the launcher's configuration file! - - - true - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + + This only applies to the launcher. Minecraft does not accept proxy settings. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + Type + + + + + + Uses your system's default proxy settings. + + + Use s&ystem settings + + + proxyGroup + + + + + + + &None + + + proxyGroup + + + + + + + &SOCKS5 + + + proxyGroup + + + + + + + &HTTP + + + proxyGroup + + + + + + + + + + &Address and Port + + + + + + + 0 + 0 + + + + + 300 + 0 + + + + 127.0.0.1 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QAbstractSpinBox::PlusMinus + + + 65535 + + + 8080 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Authentication + + + + + + &Username: + + + proxyUserEdit + + + + + + + + + + &Password: + + + proxyPassEdit + + + + + + + QLineEdit::Password + + + + + + + Note: Proxy username and password are stored in plain text inside the launcher's configuration file! + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + +
    + + proxyDefaultBtn + proxyNoneBtn + proxySOCKS5Btn + proxyHTTPBtn + proxyAddrEdit + proxyPortEdit + proxyUserEdit + proxyPassEdit + diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp new file mode 100644 index 000000000..fb07a768b --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DataPackPage.h" +#include "minecraft/PackProfile.h" +#include "ui_ExternalResourcesPage.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" + +DataPackPage::DataPackPage(BaseInstance* instance, DataPackFolderModel* model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) +{ + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download data packs from online mod platforms")); + ui->actionDownloadItem->setEnabled(true); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected data packs (all data packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &DataPackPage::updateDataPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &DataPackPage::updateDataPacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &DataPackPage::deleteDataPackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a data pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &DataPackPage::changeDataPackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); +} + +void DataPackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& dp = m_model->at(row); + ui->frame->updateWithDataPack(dp); +} + +void DataPackPage::downloadDataPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &DataPackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +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) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +void DataPackPage::updateDataPacks() +{ + 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!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(this, tr("Confirm Update"), + tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, { ModPlatform::ModLoaderType::DataPack }); + update_dialog.checkCandidates(); + + if (update_dialog.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) { + message = tr("All data packs are up-to-date! :)"); + } else { + message = tr("All selected data packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + 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) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void DataPackPage::deleteDataPackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedDataPacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 data packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void DataPackPage::changeDataPackVersion() +{ + 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!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance); + 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) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) +{ + 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) + m_container->refreshContainer(); + }); + + connect(instance->settings()->getSetting("GlobalDataPacksPath").get(), &Setting::SettingChanged, this, + &GlobalDataPackPage::updateContent); +} + +QString GlobalDataPackPage::displayName() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->displayName(); +} + +QIcon GlobalDataPackPage::icon() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->icon(); +} + +QString GlobalDataPackPage::helpPage() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->helpPage(); +} + +bool GlobalDataPackPage::shouldDisplay() const +{ + return m_instance->settings()->get("GlobalDataPacksEnabled").toBool(); +} + +bool GlobalDataPackPage::apply() +{ + return m_underlyingPage == nullptr || m_underlyingPage->apply(); +} + +void GlobalDataPackPage::openedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->openedImpl(); +} + +void GlobalDataPackPage::closedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->closedImpl(); +} + +void GlobalDataPackPage::updateContent() +{ + if (m_underlyingPage != nullptr) { + if (m_container->selectedPage() == this) + m_underlyingPage->closedImpl(); + + m_underlyingPage->apply(); + + layout()->removeWidget(m_underlyingPage); + + delete m_underlyingPage; + m_underlyingPage = nullptr; + } + + if (shouldDisplay()) { + m_underlyingPage = new DataPackPage(m_instance, m_instance->dataPackList()); + 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) + m_underlyingPage->openedImpl(); + + layout()->addWidget(m_underlyingPage); + } +} +void GlobalDataPackPage::setParentContainer(BasePageContainer* container) +{ + BasePage::setParentContainer(container); + updateContent(); +} diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h new file mode 100644 index 000000000..a3e6627d4 --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "ExternalResourcesPage.h" +#include "minecraft/mod/DataPackFolderModel.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +class DataPackPage : public ExternalResourcesPage { + Q_OBJECT + public: + 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"); } + QString id() const override { return "datapacks"; } + QString helpPage() const override { return "Data-packs"; } + bool shouldDisplay() const override { return true; } + + public slots: + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + void downloadDataPacks(); + void downloadDialogFinished(int result); + void updateDataPacks(); + void deleteDataPackMetadata(); + void changeDataPackVersion(); + + private: + DataPackFolderModel* m_model; + QPointer m_downloadDialog; +}; + +/** + * Syncs DataPackPage with GlobalDataPacksPath and shows/hides based on GlobalDataPacksEnabled. + */ +class GlobalDataPackPage : public QWidget, public BasePage { + public: + explicit GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent = nullptr); + + QString displayName() const override; + QIcon icon() const override; + QString id() const override { return "datapacks"; } + QString helpPage() const override; + + bool shouldDisplay() const override; + + bool apply() override; + void openedImpl() override; + void closedImpl() override; + + void setParentContainer(BasePageContainer* container) override; + + private: + void updateContent(); + QVBoxLayout* layout() { return static_cast(QWidget::layout()); } + + MinecraftInstance* m_instance; + DataPackPage* m_underlyingPage = nullptr; +}; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 8f8dab46d..da7fa3ee0 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -47,18 +47,18 @@ #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); - ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); + ui->actionsToolbar->insertSpacer(ui->actionViewFolder); m_filterModel = model->createFilterProxyModel(this); 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 @@ -74,6 +74,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->actionRemoveItem, &QAction::triggered, this, &ExternalResourcesPage::removeItem); connect(ui->actionEnableItem, &QAction::triggered, this, &ExternalResourcesPage::enableItem); connect(ui->actionDisableItem, &QAction::triggered, this, &ExternalResourcesPage::disableItem); + connect(ui->actionViewHomepage, &QAction::triggered, this, &ExternalResourcesPage::viewHomepage); connect(ui->actionViewConfigs, &QAction::triggered, this, &ExternalResourcesPage::viewConfigs); connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); @@ -81,16 +82,28 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); auto selection_model = ui->treeView->selectionModel(); - connect(selection_model, &QItemSelectionModel::currentChanged, this, &ExternalResourcesPage::current); + + connect(selection_model, &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& current, const QModelIndex& previous) { + if (!current.isValid()) { + ui->frame->clear(); + return; + } + + updateFrame(current, previous); + }); + auto updateExtra = [this]() { if (updateExtraInfo) updateExtraInfo(id(), extraHeaderInfoString()); }; + connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); - connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); - connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra); + connect(model, &ResourceFolderModel::updateFinished, this, updateExtra); + connect(model, &ResourceFolderModel::parseFinished, this, updateExtra); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + connect(selection_model, &QItemSelectionModel::selectionChanged, 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); @@ -99,6 +112,8 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_model->loadColumns(ui->treeView); connect(ui->treeView->header(), &QHeaderView::sectionResized, this, [this] { m_model->saveColumns(ui->treeView); }); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + updateActions(); } ExternalResourcesPage::~ExternalResourcesPage() @@ -132,19 +147,16 @@ void ExternalResourcesPage::openedImpl() m_model->startWatching(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->actionsToolbar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ExternalResourcesPage::closedImpl() { m_model->stopWatching(); - m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState()); + m_wide_bar_setting->set(QString::fromUtf8(ui->actionsToolbar->getVisibilityState().toBase64())); } void ExternalResourcesPage::retranslate() @@ -275,20 +287,20 @@ void ExternalResourcesPage::enableItem() void ExternalResourcesPage::disableItem() { - if (m_instance != nullptr && m_instance->isRunning()) { - auto response = CustomMessageBox::selectable(this, tr("Confirm disable"), - tr("If you disable this resource while the game is running it may crash your game.\n" - "Are you sure you want to do this?"), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - - if (response != QMessageBox::Yes) - return; - } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); } +void ExternalResourcesPage::viewHomepage() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + for (auto resource : m_model->selectedResources(selection)) { + auto url = resource->homepage(); + if (!url.isEmpty()) + DesktopServices::openUrl(url); + } +} + void ExternalResourcesPage::viewConfigs() { DesktopServices::openPath(m_instance->instanceConfigFolder(), true); @@ -299,23 +311,32 @@ void ExternalResourcesPage::viewFolder() DesktopServices::openPath(m_model->dir().absolutePath(), true); } -bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous) +void ExternalResourcesPage::updateActions() { - if (!current.isValid()) { - ui->frame->clear(); - return false; - } + const bool hasSelection = ui->treeView->selectionModel()->hasSelection(); + const QModelIndexList selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + const QList selectedResources = m_model->selectedResources(selection); + + ui->actionUpdateItem->setEnabled(!m_model->empty()); + ui->actionResetItemMetadata->setEnabled(hasSelection); - return onSelectionChanged(current, previous); + ui->actionChangeVersion->setEnabled(selectedResources.size() == 1 && selectedResources[0]->metadata() != nullptr); + + ui->actionRemoveItem->setEnabled(hasSelection); + ui->actionEnableItem->setEnabled(hasSelection); + ui->actionDisableItem->setEnabled(hasSelection); + + ui->actionViewHomepage->setEnabled(hasSelection && std::any_of(selectedResources.begin(), selectedResources.end(), + [](Resource* resource) { return !resource->homepage().isEmpty(); })); + ui->actionExportMetadata->setEnabled(!m_model->empty()); } -bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ExternalResourcesPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); Resource const& resource = m_model->at(row); ui->frame->updateWithResource(resource); - return true; } QString ExternalResourcesPage::extraHeaderInfoString() diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d29be0fc3..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; @@ -42,9 +42,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; public slots: - bool current(const QModelIndex& current, const QModelIndex& previous); - - virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous); + virtual void updateActions(); + virtual void updateFrame(const QModelIndex& current, const QModelIndex& previous); protected slots: void itemActivated(const QModelIndex& index); @@ -57,6 +56,8 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { virtual void enableItem(); virtual void disableItem(); + virtual void viewHomepage(); + virtual void viewFolder(); virtual void viewConfigs(); @@ -67,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/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 9d6f61db0..c6955d0ce 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -24,31 +24,7 @@ 0 - - - - - - - - - Filter: - - - - - - - - - - 0 - 0 - - - - - + @@ -60,13 +36,30 @@ true - QAbstractItemView::DropOnly + QAbstractItemView::DragDropMode::DropOnly true + + + + + 0 + 0 + + + + + + + + Search + + +
    @@ -74,7 +67,7 @@ Actions
    - Qt::ToolButtonTextOnly + Qt::ToolButtonStyle::ToolButtonIconOnly true @@ -90,39 +83,49 @@ - + +
    - &Add + &Add File - Add + Add a locally downloaded file. + + false + &Remove - Remove selected item + Remove all selected items. + + false + &Enable - Enable selected item + Enable all selected items. + + false + &Disable - Disable selected item + Disable all selected items. @@ -137,6 +140,9 @@ View &Folder + + Open the folder in the system file manager. + @@ -146,40 +152,70 @@ &Download - Download a new resource + Download resources from online mod platforms. - + false - Visit mod's page + Check for &Updates - Go to mods home page + Try to check or update all selected resources (all resources if none are selected). - + + + Reset Update Metadata + + + QAction::MenuRole::NoRole + + + + + Verify Dependencies + + + QAction::MenuRole::NoRole + + + - true + false - Check for &Updates + Export List - Try to check or update all selected resources (all resources if none are selected) + Export resource's metadata to text. - + - true + false + + + Change Version + + + Change a resource's version. + + + QAction::MenuRole::NoRole + + + + + false - Export modlist + View Homepage - Export mod's metadata to text + View the homepages of all selected items.
    @@ -203,7 +239,6 @@ treeView - filterEdit diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index a132843e7..43f91976c 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -38,7 +38,6 @@ #include #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -59,7 +58,7 @@ class GameOptionsPage : public QWidget, public BasePage { void closedImpl() override; virtual QString displayName() const override { return tr("Game Options"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("settings"); } + virtual QIcon icon() const override { return QIcon::fromTheme("settings"); } virtual QString id() const override { return "gameoptions"; } virtual QString helpPage() const override { return "Game-Options-management"; } void retranslate() override; diff --git a/launcher/ui/pages/instance/GameOptionsPage.ui b/launcher/ui/pages/instance/GameOptionsPage.ui deleted file mode 100644 index f0a5ce0ee..000000000 --- a/launcher/ui/pages/instance/GameOptionsPage.ui +++ /dev/null @@ -1,88 +0,0 @@ - - - GameOptionsPage - - - - 0 - 0 - 706 - 575 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - 0 - - - - Tab 1 - - - - - - - 0 - 0 - - - - true - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - 64 - 64 - - - - false - - - false - - - - - - - - - - - tabWidget - optionsView - - - - diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp deleted file mode 100644 index af66209de..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ /dev/null @@ -1,615 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "InstanceSettingsPage.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/WorldList.h" -#include "settings/Setting.h" -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/java/InstallJavaDialog.h" -#include "ui_InstanceSettingsPage.h" - -#include -#include -#include - -#include - -#include "ui/dialogs/VersionSelectDialog.h" -#include "ui/widgets/CustomCommands.h" - -#include "Application.h" -#include "BuildConfig.h" -#include "JavaCommon.h" -#include "minecraft/auth/AccountList.h" - -#include "FileSystem.h" -#include "java/JavaInstallList.h" -#include "java/JavaUtils.h" - -InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) - : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) -{ - m_settings = inst->settings(); - ui->setupUi(this); - - ui->javaDownloadBtn->setHidden(!BuildConfig.JAVA_DOWNLOADER_ENABLED); - - connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); - connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); - connect(ui->instanceAccountSelector, QOverload::of(&QComboBox::currentIndexChanged), this, - &InstanceSettingsPage::changeInstanceAccount); - - connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeGLFWChanged); - connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeOpenALChanged); - - auto mInst = dynamic_cast(inst); - m_world_quickplay_supported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); - if (m_world_quickplay_supported) { - auto worlds = mInst->worldList(); - worlds->update(); - for (const auto& world : worlds->allWorlds()) { - ui->worldsCb->addItem(world.folderName()); - } - } else { - ui->worldsCb->hide(); - ui->worldJoinButton->hide(); - ui->serverJoinAddressButton->setChecked(true); - ui->serverJoinAddress->setEnabled(true); - ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); - } - connect(ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { - if (m_instance->settings()->get("JavaPath").toString() != newValue) { - m_instance->settings()->set("AutomaticJava", false); - } - }); - - loadSettings(); - - updateThresholds(); -} - -InstanceSettingsPage::~InstanceSettingsPage() -{ - delete ui; -} - -void InstanceSettingsPage::globalSettingsButtonClicked(bool) -{ - switch (ui->settingsTabs->currentIndex()) { - case 0: - APPLICATION->ShowGlobalSettings(this, "java-settings"); - return; - case 2: - APPLICATION->ShowGlobalSettings(this, "custom-commands"); - return; - case 3: - APPLICATION->ShowGlobalSettings(this, "environment-variables"); - return; - default: - APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); - return; - } -} - -bool InstanceSettingsPage::apply() -{ - applySettings(); - return true; -} - -void InstanceSettingsPage::applySettings() -{ - SettingsObject::Lock lock(m_settings); - - // Miscellaneous - bool miscellaneous = ui->miscellaneousSettingsBox->isChecked(); - m_settings->set("OverrideMiscellaneous", miscellaneous); - if (miscellaneous) { - m_settings->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); - m_settings->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - } else { - m_settings->reset("CloseAfterLaunch"); - m_settings->reset("QuitAfterGameStop"); - } - - // Console - bool console = ui->consoleSettingsBox->isChecked(); - m_settings->set("OverrideConsole", console); - if (console) { - m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked()); - m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); - m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); - } else { - m_settings->reset("ShowConsole"); - m_settings->reset("AutoCloseConsole"); - m_settings->reset("ShowConsoleOnError"); - } - - // Window Size - bool window = ui->windowSizeGroupBox->isChecked(); - m_settings->set("OverrideWindow", window); - if (window) { - m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); - m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); - m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - } else { - m_settings->reset("LaunchMaximized"); - m_settings->reset("MinecraftWinWidth"); - m_settings->reset("MinecraftWinHeight"); - } - - // Memory - bool memory = ui->memoryGroupBox->isChecked(); - m_settings->set("OverrideMemory", memory); - if (memory) { - int min = ui->minMemSpinBox->value(); - int max = ui->maxMemSpinBox->value(); - if (min < max) { - m_settings->set("MinMemAlloc", min); - m_settings->set("MaxMemAlloc", max); - } else { - m_settings->set("MinMemAlloc", max); - m_settings->set("MaxMemAlloc", min); - } - m_settings->set("PermGen", ui->permGenSpinBox->value()); - } else { - m_settings->reset("MinMemAlloc"); - m_settings->reset("MaxMemAlloc"); - m_settings->reset("PermGen"); - } - - // Java Install Settings - bool javaInstall = ui->javaSettingsGroupBox->isChecked(); - m_settings->set("OverrideJavaLocation", javaInstall); - if (javaInstall) { - m_settings->set("JavaPath", ui->javaPathTextBox->text()); - m_settings->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); - } else { - m_settings->reset("JavaPath"); - m_settings->reset("IgnoreJavaCompatibility"); - } - - // Java arguments - bool javaArgs = ui->javaArgumentsGroupBox->isChecked(); - m_settings->set("OverrideJavaArgs", javaArgs); - if (javaArgs) { - m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); - } else { - m_settings->reset("JvmArgs"); - } - - // Custom Commands - bool custcmd = ui->customCommands->checked(); - m_settings->set("OverrideCommands", custcmd); - if (custcmd) { - m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand()); - m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand()); - m_settings->set("PostExitCommand", ui->customCommands->postexitCommand()); - } else { - m_settings->reset("PreLaunchCommand"); - m_settings->reset("WrapperCommand"); - m_settings->reset("PostExitCommand"); - } - - // Environment Variables - auto env = ui->environmentVariables->override(); - m_settings->set("OverrideEnv", env); - if (env) - m_settings->set("Env", ui->environmentVariables->value()); - else - m_settings->reset("Env"); - - // Workarounds - bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); - m_settings->set("OverrideNativeWorkarounds", workarounds); - if (workarounds) { - m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); - m_settings->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); - m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); - m_settings->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); - } else { - m_settings->reset("UseNativeGLFW"); - m_settings->reset("CustomGLFWPath"); - m_settings->reset("UseNativeOpenAL"); - m_settings->reset("CustomOpenALPath"); - } - - // Performance - bool performance = ui->perfomanceGroupBox->isChecked(); - m_settings->set("OverridePerformance", performance); - if (performance) { - m_settings->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); - m_settings->set("EnableMangoHud", ui->enableMangoHud->isChecked()); - m_settings->set("UseDiscreteGpu", ui->useDiscreteGpuCheck->isChecked()); - m_settings->set("UseZink", ui->useZink->isChecked()); - - } else { - m_settings->reset("EnableFeralGamemode"); - m_settings->reset("EnableMangoHud"); - m_settings->reset("UseDiscreteGpu"); - m_settings->reset("UseZink"); - } - - // Game time - bool gameTime = ui->gameTimeGroupBox->isChecked(); - m_settings->set("OverrideGameTime", gameTime); - if (gameTime) { - m_settings->set("ShowGameTime", ui->showGameTime->isChecked()); - m_settings->set("RecordGameTime", ui->recordGameTime->isChecked()); - } else { - m_settings->reset("ShowGameTime"); - m_settings->reset("RecordGameTime"); - } - - // Join server on launch - bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); - m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); - if (joinServerOnLaunch) { - if (ui->serverJoinAddressButton->isChecked() || !m_world_quickplay_supported) { - m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); - m_settings->reset("JoinWorldOnLaunch"); - } else { - m_settings->set("JoinWorldOnLaunch", ui->worldsCb->currentText()); - m_settings->reset("JoinServerOnLaunchAddress"); - } - } else { - m_settings->reset("JoinServerOnLaunchAddress"); - m_settings->reset("JoinWorldOnLaunch"); - } - - // Use an account for this instance - bool useAccountForInstance = ui->instanceAccountGroupBox->isChecked(); - m_settings->set("UseAccountForInstance", useAccountForInstance); - if (!useAccountForInstance) { - m_settings->reset("InstanceAccountId"); - } - - bool overrideLegacySettings = ui->legacySettingsGroupBox->isChecked(); - m_settings->set("OverrideLegacySettings", overrideLegacySettings); - if (overrideLegacySettings) { - m_settings->set("OnlineFixes", ui->onlineFixes->isChecked()); - } else { - m_settings->reset("OnlineFixes"); - } - - // FIXME: This should probably be called by a signal instead - m_instance->updateRuntimeContext(); -} - -void InstanceSettingsPage::loadSettings() -{ - // Miscellaneous - ui->miscellaneousSettingsBox->setChecked(m_settings->get("OverrideMiscellaneous").toBool()); - ui->closeAfterLaunchCheck->setChecked(m_settings->get("CloseAfterLaunch").toBool()); - ui->quitAfterGameStopCheck->setChecked(m_settings->get("QuitAfterGameStop").toBool()); - - // Console - ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); - ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); - ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool()); - ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool()); - - // Window Size - ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool()); - ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool()); - ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt()); - ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt()); - - // Memory - ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool()); - int min = m_settings->get("MinMemAlloc").toInt(); - int max = m_settings->get("MaxMemAlloc").toInt(); - if (min < max) { - ui->minMemSpinBox->setValue(min); - ui->maxMemSpinBox->setValue(max); - } else { - ui->minMemSpinBox->setValue(max); - ui->maxMemSpinBox->setValue(min); - } - ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt()); - bool permGenVisible = m_settings->get("PermGenVisible").toBool(); - ui->permGenSpinBox->setVisible(permGenVisible); - ui->labelPermGen->setVisible(permGenVisible); - ui->labelPermgenNote->setVisible(permGenVisible); - - // Java Settings - bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool(); - bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool(); - - connect(m_settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, - [this] { ui->javaSettingsGroupBox->setChecked(m_settings->get("OverrideJavaLocation").toBool()); }); - ui->javaSettingsGroupBox->setChecked(overrideLocation); - ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); - connect(m_settings->getSetting("JavaPath").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, - [this] { ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); }); - - ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); - - ui->javaArgumentsGroupBox->setChecked(overrideArgs); - ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString()); - - // Custom commands - ui->customCommands->initialize(true, m_settings->get("OverrideCommands").toBool(), m_settings->get("PreLaunchCommand").toString(), - m_settings->get("WrapperCommand").toString(), m_settings->get("PostExitCommand").toString()); - - // Environment variables - ui->environmentVariables->initialize(true, m_settings->get("OverrideEnv").toBool(), m_settings->get("Env").toMap()); - - // Workarounds - ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); - ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); - ui->lineEditGLFWPath->setText(m_settings->get("CustomGLFWPath").toString()); -#ifdef Q_OS_LINUX - ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); -#else - ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); -#endif - ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); - ui->lineEditOpenALPath->setText(m_settings->get("CustomOpenALPath").toString()); -#ifdef Q_OS_LINUX - ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); -#else - ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); -#endif - - // Performance - ui->perfomanceGroupBox->setChecked(m_settings->get("OverridePerformance").toBool()); - ui->enableFeralGamemodeCheck->setChecked(m_settings->get("EnableFeralGamemode").toBool()); - ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool()); - ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool()); - ui->useZink->setChecked(m_settings->get("UseZink").toBool()); - -#if !defined(Q_OS_LINUX) - ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false); -#endif - - if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { - ui->enableFeralGamemodeCheck->setDisabled(true); - ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); - } - - if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { - ui->enableMangoHud->setDisabled(true); - ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); - } - - // Miscellanous - ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); - ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); - ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); - - ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); - - if (auto server = m_settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { - ui->serverJoinAddress->setText(server); - ui->serverJoinAddressButton->setChecked(true); - ui->worldJoinButton->setChecked(false); - ui->serverJoinAddress->setEnabled(true); - ui->worldsCb->setEnabled(false); - } else if (auto world = m_settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_world_quickplay_supported) { - ui->worldsCb->setCurrentText(world); - ui->serverJoinAddressButton->setChecked(false); - ui->worldJoinButton->setChecked(true); - ui->serverJoinAddress->setEnabled(false); - ui->worldsCb->setEnabled(true); - } else { - ui->serverJoinAddressButton->setChecked(true); - ui->worldJoinButton->setChecked(false); - ui->serverJoinAddress->setEnabled(true); - ui->worldsCb->setEnabled(false); - } - - ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); - updateAccountsMenu(); - - ui->legacySettingsGroupBox->setChecked(m_settings->get("OverrideLegacySettings").toBool()); - ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); -} - -void InstanceSettingsPage::on_javaDownloadBtn_clicked() -{ - auto jdialog = new Java::InstallDialog({}, m_instance, this); - jdialog->exec(); -} - -void InstanceSettingsPage::on_javaDetectBtn_clicked() -{ - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; - } - - JavaInstallPtr java; - - VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); - vselect.setResizeOn(2); - vselect.exec(); - - if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { - java = std::dynamic_pointer_cast(vselect.selectedVersion()); - ui->javaPathTextBox->setText(java->path); - bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool(); - ui->permGenSpinBox->setVisible(visible); - ui->labelPermGen->setVisible(visible); - ui->labelPermgenNote->setVisible(visible); - m_settings->set("PermGenVisible", visible); - - if (!java->is_64bit && m_settings->get("MaxMemAlloc").toInt() > 2048) { - CustomMessageBox::selectable(this, tr("Confirm Selection"), - tr("You selected a 32-bit version of Java.\n" - "This installation does not support more than 2048MiB of RAM.\n" - "Please make sure that the maximum memory value is lower."), - QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) - ->exec(); - } - } -} - -void InstanceSettingsPage::on_javaBrowseBtn_clicked() -{ - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); - - // do not allow current dir - it's dirty. Do not allow dirs that don't exist - if (raw_path.isEmpty()) { - return; - } - QString cooked_path = FS::NormalizePath(raw_path); - - QFileInfo javaInfo(cooked_path); - if (!javaInfo.exists() || !javaInfo.isExecutable()) { - return; - } - ui->javaPathTextBox->setText(cooked_path); - - // custom Java could be anything... enable perm gen option - ui->permGenSpinBox->setVisible(true); - ui->labelPermGen->setVisible(true); - ui->labelPermgenNote->setVisible(true); - m_settings->set("PermGenVisible", true); -} - -void InstanceSettingsPage::on_javaTestBtn_clicked() -{ - if (checker) { - return; - } - checker.reset(new JavaCommon::TestCheck(this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), - ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); - connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); - checker->run(); -} - -void InstanceSettingsPage::onUseNativeGLFWChanged(bool checked) -{ - ui->lineEditGLFWPath->setEnabled(checked); -} - -void InstanceSettingsPage::onUseNativeOpenALChanged(bool checked) -{ - ui->lineEditOpenALPath->setEnabled(checked); -} - -void InstanceSettingsPage::updateAccountsMenu() -{ - ui->instanceAccountSelector->clear(); - auto accounts = APPLICATION->accounts(); - int accountIndex = accounts->findAccountByProfileId(m_settings->get("InstanceAccountId").toString()); - - for (int i = 0; i < accounts->count(); i++) { - MinecraftAccountPtr account = accounts->at(i); - ui->instanceAccountSelector->addItem(getFaceForAccount(account), account->profileName(), i); - if (i == accountIndex) - ui->instanceAccountSelector->setCurrentIndex(i); - } -} - -QIcon InstanceSettingsPage::getFaceForAccount(MinecraftAccountPtr account) -{ - if (auto face = account->getFace(); !face.isNull()) { - return face; - } - - return APPLICATION->getThemedIcon("noaccount"); -} - -void InstanceSettingsPage::changeInstanceAccount(int index) -{ - auto accounts = APPLICATION->accounts(); - if (index != -1 && accounts->at(index) && ui->instanceAccountGroupBox->isChecked()) { - auto account = accounts->at(index); - m_settings->set("InstanceAccountId", account->profileId()); - } -} - -void InstanceSettingsPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) -{ - updateThresholds(); -} - -void InstanceSettingsPage::checkerFinished() -{ - checker.reset(); -} - -void InstanceSettingsPage::retranslate() -{ - ui->retranslateUi(this); - ui->customCommands->retranslate(); // TODO: why is this seperate from the others? - ui->environmentVariables->retranslate(); -} - -void InstanceSettingsPage::updateThresholds() -{ - auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; - unsigned int maxMem = ui->maxMemSpinBox->value(); - unsigned int minMem = ui->minMemSpinBox->value(); - - QString iconName; - - if (maxMem >= sysMiB) { - iconName = "status-bad"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (maxMem > (sysMiB * 0.9)) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (maxMem < minMem) { - iconName = "status-yellow"; - ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); - } else { - iconName = "status-good"; - ui->labelMaxMemIcon->setToolTip(""); - } - - { - auto height = ui->labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - ui->labelMaxMemIcon->setPixmap(pix); - } -} - -void InstanceSettingsPage::on_serverJoinAddressButton_toggled(bool checked) -{ - ui->serverJoinAddress->setEnabled(checked); -} - -void InstanceSettingsPage::on_worldJoinButton_toggled(bool checked) -{ - ui->worldsCb->setEnabled(checked); -} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 6499f9e8f..79d5944eb 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -36,62 +36,27 @@ #pragma once #include - -#include -#include -#include "Application.h" #include "BaseInstance.h" -#include "JavaCommon.h" -#include "java/JavaChecker.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/MinecraftSettingsWidget.h" -class JavaChecker; -namespace Ui { -class InstanceSettingsPage; -} - -class InstanceSettingsPage : public QWidget, public BasePage { +class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit InstanceSettingsPage(BaseInstance* inst, QWidget* parent = 0); - virtual ~InstanceSettingsPage(); - virtual QString displayName() const override { return tr("Settings"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("instance-settings"); } - virtual QString id() const override { return "settings"; } - virtual bool apply() override; - virtual QString helpPage() const override { return "Instance-settings"; } - void retranslate() override; - - void updateThresholds(); - - private slots: - void on_javaDetectBtn_clicked(); - void on_javaTestBtn_clicked(); - void on_javaBrowseBtn_clicked(); - void on_javaDownloadBtn_clicked(); - void on_maxMemSpinBox_valueChanged(int i); - void on_serverJoinAddressButton_toggled(bool checked); - void on_worldJoinButton_toggled(bool checked); - - void onUseNativeGLFWChanged(bool checked); - void onUseNativeOpenALChanged(bool checked); - - void applySettings(); - void loadSettings(); - - void checkerFinished(); - - void globalSettingsButtonClicked(bool checked); - - void updateAccountsMenu(); - QIcon getFaceForAccount(MinecraftAccountPtr account); - void changeInstanceAccount(int index); - - private: - Ui::InstanceSettingsPage* ui; - BaseInstance* m_instance; - SettingsObjectPtr m_settings; - unique_qobject_ptr checker; - bool m_world_quickplay_supported; + explicit InstanceSettingsPage(MinecraftInstance* instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(instance, parent) + { + connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); + connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceSettingsPage::loadSettings); + } + ~InstanceSettingsPage() override {} + QString displayName() const override { return tr("Settings"); } + QIcon icon() const override { return QIcon::fromTheme("instance-settings"); } + QString id() const override { return "settings"; } + bool apply() override + { + saveSettings(); + return true; + } + QString helpPage() const override { return "Instance-settings"; } }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui deleted file mode 100644 index 4905eae87..000000000 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ /dev/null @@ -1,814 +0,0 @@ - - - InstanceSettingsPage - - - - 0 - 0 - 691 - 581 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Open Global Settings - - - The settings here are overrides for global settings. - - - - - - - 0 - - - - Java - - - - - - true - - - Java insta&llation - - - true - - - false - - - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - Skip Java compatibility checks - - - - - - - - - - - - Browse - - - - - - - - - - - Download Java - - - - - - - Auto-detect... - - - - - - - Test - - - - - - - - - - - - true - - - Memor&y - - - true - - - false - - - - - - PermGen: - - - - - - - Minimum memory allocation: - - - - - - - Maximum memory allocation: - - - - - - - Note: Permgen is set automatically by Java 8 and later - - - - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 999999999 - - - 8 - - - 64 - - - - - - - - - - Qt::AlignCenter - - - maxMemSpinBox - - - - - - - - - - true - - - Java argumen&ts - - - true - - - false - - - - - - - - - - - - - Game windows - - - - - - true - - - Game Window - - - true - - - false - - - - - - Start Minecraft maximized - - - - - - - - - Window height: - - - - - - - Window width: - - - - - - - 1 - - - 65536 - - - 1 - - - 854 - - - - - - - 1 - - - 65536 - - - 480 - - - - - - - - - - - - true - - - Conso&le Settings - - - true - - - false - - - - - - Show console while the game is running - - - - - - - Automatically close console when the game quits - - - - - - - Show console when the game crashes - - - - - - - - - - Miscellaneous - - - true - - - false - - - - - - Close the launcher after game window opens - - - - - - - Quit the launcher after game window closes - - - - - - - - - - Qt::Vertical - - - - 88 - 125 - - - - - - - - - Custom commands - - - - - - - - - - Environment variables - - - - - - - - - - Workarounds - - - - - - true - - - Native libraries - - - true - - - false - - - - - - Use system installation of OpenAL - - - - - - - &GLFW library path - - - lineEditGLFWPath - - - - - - - Use system installation of GLFW - - - - - - - false - - - - - - - &OpenAL library path - - - lineEditOpenALPath - - - - - - - false - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Performance - - - - - - true - - - Performance - - - true - - - false - - - - - - <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> - - - Enable Feral GameMode - - - - - - - <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> - - - Enable MangoHud - - - - - - - <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> - - - Use discrete GPU - - - - - - - Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used. - - - Use Zink - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Miscellaneous - - - - - - Legacy settings - - - true - - - false - - - - - - <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html> - - - Enable online fixes (experimental) - - - - - - - - - - true - - - Override global game time settings - - - true - - - false - - - - - - Show time spent playing this instance - - - - - - - Record time spent playing this instance - - - - - - - - - - Set a target to join on launch - - - true - - - false - - - - - - Server address: - - - - - - - - - - Singleplayer world - - - - - - - - - - - - - Override default account - - - true - - - false - - - - - - - - - 0 - 0 - - - - Account: - - - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - CustomCommands - QWidget -
    ui/widgets/CustomCommands.h
    - 1 -
    - - EnvironmentVariables - QWidget -
    ui/widgets/EnvironmentVariables.h
    - 1 -
    -
    - - openGlobalJavaSettingsButton - settingsTabs - javaSettingsGroupBox - javaPathTextBox - javaBrowseBtn - javaDownloadBtn - javaDetectBtn - javaTestBtn - skipCompatibilityCheckbox - memoryGroupBox - minMemSpinBox - maxMemSpinBox - permGenSpinBox - javaArgumentsGroupBox - jvmArgsTextBox - windowSizeGroupBox - maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox - consoleSettingsBox - showConsoleCheck - autoCloseConsoleCheck - showConsoleErrorCheck - nativeWorkaroundsGroupBox - useNativeGLFWCheck - useNativeOpenALCheck - showGameTime - recordGameTime - miscellaneousSettingsBox - closeAfterLaunchCheck - quitAfterGameStopCheck - perfomanceGroupBox - enableFeralGamemodeCheck - enableMangoHud - useDiscreteGpuCheck - gameTimeGroupBox - serverJoinGroupBox - serverJoinAddress - instanceAccountGroupBox - instanceAccountSelector - - - -
    diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 6a4888f9c..770698961 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,95 +52,85 @@ #include -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override - { - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + MessageLevel level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); - if (result.isValid()) - return result; + if (result.isValid()) + return result; - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); + break; + } + case Qt::BackgroundRole: { + MessageLevel level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); - if (result.isValid()) - return result; + if (result.isValid()) + return result; - break; - } + break; } - - return QIdentityProxyModel::data(index, role); } - void setFont(QFont font) { m_font = font; } + return QIdentityProxyModel::data(index, role); +} - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const - { - QModelIndex parentIndex = parent(start); - auto compare = [&](int r) -> QModelIndex { - QModelIndex idx = index(r, start.column(), parentIndex); - if (!idx.isValid() || idx == start) { - return QModelIndex(); - } - QVariant v = data(idx, Qt::DisplayRole); - QString t = v.toString(); - if (t.contains(value, Qt::CaseInsensitive)) - return idx; +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { return QModelIndex(); - }; - if (reverse) { - int from = start.row(); - int to = 0; - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r >= to); --r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = rowCount() - 1; - to = start.row(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if (reverse) { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; } - } else { - int from = start.row(); - int to = rowCount(parentIndex); - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r < to); ++r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = 0; - to = start.row(); + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; } + // prepare for the next iteration + from = 0; + to = start.row(); } - return QModelIndex(); } + return QModelIndex(); +} - private: - QFont m_font; -}; - -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); - ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); @@ -163,16 +153,16 @@ 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); - connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + connect(findShortcut, &QShortcut::activated, this, &LogPage::findActivated); auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); - connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + connect(findNextShortcut, &QShortcut::activated, this, &LogPage::findNextActivated); + connect(ui->searchBar, &QLineEdit::returnPressed, this, &LogPage::on_findButton_clicked); auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); + connect(findPreviousShortcut, &QShortcut::activated, this, &LogPage::findPreviousActivated); } LogPage::~LogPage() @@ -189,6 +179,13 @@ void LogPage::modelStateToUI() ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } + if (m_model->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { @@ -202,10 +199,11 @@ void LogPage::UIToModelState() return; } m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } -void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial) +void LogPage::setInstanceLaunchTaskChanged(LaunchTask* proc, bool initial) { m_process = proc; if (m_process) { @@ -222,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); } @@ -291,6 +289,14 @@ void LogPage::on_wrapCheckbox_clicked(bool checked) m_model->setLineWrap(checked); } +void LogPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); +} + void LogPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 6c259891d..ef93f2cc0 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -35,9 +35,9 @@ #pragma once +#include #include -#include #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" @@ -46,16 +46,27 @@ namespace Ui { class LogPage; } class QTextCharFormat; -class LogFormatProxyModel; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; class LogPage : public QWidget, public BasePage { Q_OBJECT 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 APPLICATION->getThemedIcon("log"); } + virtual QIcon icon() const override { return QIcon::fromTheme("log"); } virtual QString id() const override { return "console"; } virtual bool apply() override; virtual QString helpPage() const override { return "Minecraft-Logs"; } @@ -70,23 +81,24 @@ class LogPage : public QWidget, public BasePage { void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); 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/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index 31bb368c8..2362e19c0 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -10,151 +10,153 @@ 782 - + 0 0 - - 0 - 0 - - - - 0 + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + Upload + + + + + + + Clear the log + + + Clear + + + + + + + + + + 0 + 0 + + + + Find + + + + + + + + 0 + 0 + + + + Scroll all the way to bottom + + + Bottom + + + + + + + Qt::Vertical + + + + + + + Search - - - Tab 1 - - - - - - false - - - true - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - false - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - Upload - - - - - - - Clear the log - - - Clear - - - - - - - - - Search: - - - - - - - Find - - - - - - - - - - Scroll all the way to bottom - - - Bottom - - - - - - - Qt::Vertical - - - - - @@ -167,14 +169,13 @@ - tabWidget trackLogCheckbox wrapCheckbox + colorCheckbox btnCopy btnPaste btnClear text - searchBar findButton diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 0fccd1d33..230cf1bdf 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -6,12 +6,14 @@ #include #include #include +#include "modplatform/ModIndex.h" #include "ui_ManagedPackPage.h" #include #include #include #include +#include #include "Application.h" #include "BuildConfig.h" @@ -22,8 +24,6 @@ #include "Markdown.h" #include "StringUtils.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" - #include "ui/InstanceWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -168,7 +168,7 @@ QString ManagedPackPage::displayName() const QIcon ManagedPackPage::icon() const { - return APPLICATION->getThemedIcon(m_inst->getManagedPackType()); + return QIcon::fromTheme(m_inst->getManagedPackType()); } QString ManagedPackPage::helpPage() const @@ -213,7 +213,7 @@ bool ManagedPackPage::runUpdateTask(InstanceTask* task) void ManagedPackPage::suggestVersion() { - ui->updateButton->setText(tr("Update pack")); + ui->updateButton->setText(tr("Update Pack")); ui->updateButton->setDisabled(false); } @@ -239,7 +239,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); - connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &ModrinthManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::updateFromFile); } @@ -256,62 +256,38 @@ void ModrinthManagedPackPage::parseManagedPack() if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); - m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); - auto response = std::make_shared(); - - QString id = m_inst->getManagedPackID(); - - m_fetch_job->addNetAction( - Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - - QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - setFailState(); - - return; - } + ResourceAPI::Callback> callbacks{}; + m_pack = { m_inst->getManagedPackID() }; - try { - Modrinth::loadIndexedVersions(m_pack, doc); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading modrinth modpack version: " << e.cause(); - - setFailState(); - return; - } + // Use default if no callbacks are set + callbacks.on_succeed = [this](auto& doc) { + m_pack.versions = doc; + m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); - for (auto version : m_pack.versions) { - QString name = version.version; - - if (!version.name.contains(version.version)) - name = QString("%1 — %2").arg(version.name, version.version); + for (const auto& version : m_pack.versions) { + QString name = version.getVersionDisplayString(); // 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()) name = tr("%1 (Current)").arg(name); - ui->versionsComboBox->addItem(name, QVariant(version.id)); + ui->versionsComboBox->addItem(name, version.fileId); } suggestVersion(); m_loaded = true; - }); - QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); - QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + }; + callbacks.on_fail = [this](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)); ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); @@ -347,13 +323,18 @@ void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const if (m_instance_window != nullptr) m_instance_window->close(); - CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information) - ->show(); + CustomMessageBox::selectable(nullptr, tr("Update Successful"), + tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), + QMessageBox::Information) + ->show(); } else { - CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical) - ->show(); + CustomMessageBox::selectable( + nullptr, tr("Update Failed"), + tr("The instance failed to update to pack version %1. Please check launcher logs for more information.") + .arg(m_inst->getManagedPackVersionName()), + QMessageBox::Critical) + ->show(); } - } void ModrinthManagedPackPage::update() @@ -368,10 +349,10 @@ void ModrinthManagedPackPage::update() 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.id); + extra_info.insert("pack_version_id", version.fileId.toString()); extra_info.insert("original_instance_id", m_inst->id()); - auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info)); + 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)); @@ -413,7 +394,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); - connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &FlameManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &FlameManagedPackPage::updateFromFile); } @@ -447,45 +428,23 @@ void FlameManagedPackPage::parseManagedPack() if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); - m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); - auto response = std::make_shared(); - QString id = m_inst->getManagedPackID(); + m_pack = { id }; - m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); - - QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - setFailState(); + ResourceAPI::Callback> callbacks{}; - return; - } - - try { - auto obj = doc.object(); - auto data = Json::ensureArray(obj, "data"); - Flame::loadIndexedPackVersions(m_pack, data); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading flame modpack version: " << e.cause(); - - setFailState(); - return; - } + // Use default if no callbacks are set + callbacks.on_succeed = [this](auto& doc) { + m_pack.versions = doc; + m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); - for (auto version : m_pack.versions) { - QString name = version.version; + for (const auto& version : m_pack.versions) { + QString name = version.getVersionDisplayString(); if (version.fileId == m_inst->getManagedPackVersionID().toInt()) name = tr("%1 (Current)").arg(name); @@ -496,9 +455,11 @@ void FlameManagedPackPage::parseManagedPack() suggestVersion(); m_loaded = true; - }); - QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); - QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + }; + callbacks.on_fail = [this](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->start(); } @@ -519,7 +480,7 @@ void FlameManagedPackPage::suggestVersion() auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml( - StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId))); + StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId.toInt()))); ManagedPackPage::suggestVersion(); } @@ -535,7 +496,7 @@ void FlameManagedPackPage::update() QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", QString::number(version.fileId)); + 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)); diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index e8d304c6b..f319ed069 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -6,11 +6,10 @@ #include "BaseInstance.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlamePackIndex.h" #include "net/NetJob.h" @@ -37,11 +36,11 @@ class ManagedPackPage : public QWidget, public BasePage { static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr); ~ManagedPackPage() override; - [[nodiscard]] QString displayName() const override; - [[nodiscard]] QIcon icon() const override; - [[nodiscard]] QString helpPage() const override; - [[nodiscard]] QString id() const override { return "managed_pack"; } - [[nodiscard]] bool shouldDisplay() const override; + QString displayName() const override; + QIcon icon() const override; + QString helpPage() const override; + QString id() const override { return "managed_pack"; } + bool shouldDisplay() const override; void openedImpl() override; @@ -55,7 +54,7 @@ class ManagedPackPage : public QWidget, public BasePage { /** URL of the managed pack. * Not the version-specific one. */ - [[nodiscard]] virtual QString url() const { return {}; }; + virtual QString url() const { return {}; }; void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; } @@ -109,7 +108,7 @@ class GenericManagedPackPage final : public ManagedPackPage { ~GenericManagedPackPage() override = default; // TODO: We may want to show this page with some useful info at some point. - [[nodiscard]] bool shouldDisplay() const override { return false; }; + bool shouldDisplay() const override { return false; }; }; class ModrinthManagedPackPage final : public ManagedPackPage { @@ -120,8 +119,8 @@ class ModrinthManagedPackPage final : public ManagedPackPage { ~ModrinthManagedPackPage() override = default; void parseManagedPack() override; - [[nodiscard]] QString url() const override; - [[nodiscard]] QString helpPage() const override { return "modrinth-managed-pack"; } + QString url() const override; + QString helpPage() const override { return "modrinth-managed-pack"; } public slots: void suggestVersion() override; @@ -130,9 +129,9 @@ class ModrinthManagedPackPage final : public ManagedPackPage { void updateFromFile() override; private: - NetJob::Ptr m_fetch_job = nullptr; + Task::Ptr m_fetch_job = nullptr; - Modrinth::Modpack m_pack; + ModPlatform::IndexedPack m_pack; ModrinthAPI m_api; }; @@ -144,8 +143,8 @@ class FlameManagedPackPage final : public ManagedPackPage { ~FlameManagedPackPage() override = default; void parseManagedPack() override; - [[nodiscard]] QString url() const override; - [[nodiscard]] QString helpPage() const override { return "curseforge-managed-pack"; } + QString url() const override; + QString helpPage() const override { return "curseforge-managed-pack"; } public slots: void suggestVersion() override; @@ -154,8 +153,8 @@ class FlameManagedPackPage final : public ManagedPackPage { void updateFromFile() override; private: - NetJob::Ptr m_fetch_job = nullptr; + Task::Ptr m_fetch_job = nullptr; - Flame::IndexedPack m_pack; + ModPlatform::IndexedPack m_pack; FlameAPI m_api; }; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 54ff08e94..62641bc82 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -12,16 +12,16 @@ - 9 + 0 - 9 + 0 - 9 + 6 - 9 + 0 @@ -34,7 +34,7 @@ - Pack information + Pack Information @@ -42,7 +42,7 @@ - Pack name: + Pack Name: @@ -162,7 +162,7 @@ - Update from file + Update From File diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp new file mode 100644 index 000000000..7ed5dd5a8 --- /dev/null +++ b/launcher/ui/pages/instance/McClient.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include + +#include +#include "Json.h" +#include "McClient.h" + +// 7 first bits +#define SEGMENT_BITS 0x7F +// last bit +#define CONTINUE_BIT 0x80 + +McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} + +void McClient::getStatusData() +{ + qDebug() << "Connecting to socket.."; + + connect(&m_socket, &QTcpSocket::connected, this, [this]() { + qDebug() << "Connected to socket successfully"; + sendRequest(); + + connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse); + }); + + connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); }); + + m_socket.connectToHost(m_ip, m_port); +} + +void McClient::sendRequest() +{ + QByteArray data; + writeVarInt(data, 0x00); // packet ID + writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) + writeVarInt(data, m_domain.size()); // server address length + writeString(data, m_domain.toStdString()); // server address + writeFixedInt(data, m_port, 2); // server port + writeVarInt(data, 0x01); // next state + writePacketToSocket(data); // send handshake packet + + writeVarInt(data, 0x00); // packet ID + writePacketToSocket(data); // send status packet +} + +void McClient::readRawResponse() +{ + if (m_responseReadState == 2) { + return; + } + + m_resp.append(m_socket.readAll()); + if (m_responseReadState == 0 && m_resp.size() >= 5) { + m_wantedRespLength = readVarInt(m_resp); + m_responseReadState = 1; + } + + if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { + if (m_resp.size() > m_wantedRespLength) { + qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " + << m_resp.size() << " received)"; + } + parseResponse(); + m_responseReadState = 2; + } +} + +void McClient::parseResponse() +{ + qDebug() << "Received response successfully"; + + int packetID = readVarInt(m_resp); + if (packetID != 0x00) { + throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16)); + } + + Q_UNUSED(readVarInt(m_resp)); // json length + + // 'resp' should now be the JSON string + QJsonParseError parseError; + QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qDebug() << "Failed to parse JSON:" << parseError.errorString(); + emitFail(parseError.errorString()); + return; + } + emitSucceed(doc.object()); +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +void McClient::writeVarInt(QByteArray& data, int value) +{ + while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits + // Write 7 bits + data.append((value & SEGMENT_BITS) | CONTINUE_BIT); + + // Erase theses 7 bits from the value to write + // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone + value >>= 7; + } + data.append(value); +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +int McClient::readVarInt(QByteArray& data) +{ + int value = 0; + int position = 0; + char currentByte; + + while (position < 32) { + currentByte = readByte(data); + value |= (currentByte & SEGMENT_BITS) << position; + + if ((currentByte & CONTINUE_BIT) == 0) + break; + + position += 7; + } + + if (position >= 32) + throw Exception("VarInt is too big"); + + return value; +} + +char McClient::readByte(QByteArray& data) +{ + if (data.isEmpty()) { + throw Exception("No more bytes to read"); + } + + char byte = data.at(0); + data.remove(0, 1); + return byte; +} + +// write number with specified size in big endian format +void McClient::writeFixedInt(QByteArray& data, int value, int size) +{ + for (int i = size - 1; i >= 0; i--) { + data.append((value >> (i * 8)) & 0xFF); + } +} + +void McClient::writeString(QByteArray& data, const std::string& value) +{ + data.append(value.c_str()); +} + +void McClient::writePacketToSocket(QByteArray& data) +{ + // we prefix the packet with its length + QByteArray dataWithSize; + writeVarInt(dataWithSize, data.size()); + dataWithSize.append(data); + + // write it to the socket + m_socket.write(dataWithSize); + m_socket.flush(); + + data.clear(); +} + +void McClient::emitFail(QString error) +{ + qDebug() << "Minecraft server ping for status error:" << error; + emit failed(error); + emit finished(); +} + +void McClient::emitSucceed(QJsonObject data) +{ + emit succeeded(data); + emit finished(); +} diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h new file mode 100644 index 000000000..633e7aaed --- /dev/null +++ b/launcher/ui/pages/instance/McClient.h @@ -0,0 +1,53 @@ +#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); + //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data + void getStatusData(); + + private: + void sendRequest(); + //! Accumulate data until we have a full response, then call parseResponse() once + void readRawResponse(); + void parseResponse(); + + void writeVarInt(QByteArray& data, int value); + int readVarInt(QByteArray& data); + char readByte(QByteArray& data); + //! write number with specified size in big endian format + void writeFixedInt(QByteArray& data, int value, int size); + void writeString(QByteArray& data, const std::string& value); + + void writePacketToSocket(QByteArray& data); + + void emitFail(QString error); + void emitSucceed(QJsonObject data); + + signals: + void succeeded(QJsonObject data); + void failed(QString error); + void finished(); +}; diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp new file mode 100644 index 000000000..5e2b8239c --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include + +#include "McResolver.h" + +McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {} + +void McResolver::ping() +{ + pingWithDomainSRV(m_constrDomain, m_constrPort); +} + +void McResolver::pingWithDomainSRV(QString domain, int port) +{ + QDnsLookup* lookup = new QDnsLookup(this); + lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); + lookup->setType(QDnsLookup::SRV); + + connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() { + QDnsLookup* lookup = qobject_cast(sender()); + + lookup->deleteLater(); + + if (lookup->error() != QDnsLookup::NoError) { + qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString()); + pingWithDomainA(domain, port); + return; + } + + auto records = lookup->serviceRecords(); + if (records.isEmpty()) { + qDebug() << "Warning: no SRV entries found for domain, trying A record lookup"; + pingWithDomainA(domain, port); + return; + } + + const auto& firstRecord = records.at(0); + QString newDomain = firstRecord.target(); + int newPort = firstRecord.port(); + pingWithDomainA(newDomain, newPort); + }); + + lookup->lookup(); +} + +void McResolver::pingWithDomainA(QString domain, int port) +{ + QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) { + if (hostInfo.error() != QHostInfo::NoError) { + emitFail("A record lookup failed"); + return; + } + + auto records = hostInfo.addresses(); + if (records.isEmpty()) { + emitFail("No A entries found for domain"); + return; + } + + const auto& firstRecord = records.at(0); + emitSucceed(firstRecord.toString(), port); + }); +} + +void McResolver::emitFail(QString error) +{ + qDebug() << "DNS resolver error:" << error; + emit failed(error); + emit finished(); +} + +void McResolver::emitSucceed(QString ip, int port) +{ + emit succeeded(ip, port); + emit finished(); +} diff --git a/launcher/ui/pages/instance/McResolver.h b/launcher/ui/pages/instance/McResolver.h new file mode 100644 index 000000000..3dfeddc6a --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.h @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include + +// resolve the IP and port of a Minecraft server +class McResolver : public QObject { + Q_OBJECT + + QString m_constrDomain; + int m_constrPort; + + public: + explicit McResolver(QObject* parent, QString domain, int port); + void ping(); + + private: + void pingWithDomainSRV(QString domain, int port); + void pingWithDomainA(QString domain, int port); + void emitFail(QString error); + void emitSucceed(QString ip, int port); + + signals: + void succeeded(QString ip, int port); + void failed(QString error); + void finished(); +}; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8d03b6d9a..c39c5ed9d 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -38,6 +38,7 @@ #include "ModFolderPage.h" #include "ui/dialogs/ExportToModListDialog.h" +#include "ui/dialogs/InstallLoaderDialog.h" #include "ui_ExternalResourcesPage.h" #include @@ -48,121 +49,66 @@ #include #include #include +#include #include "Application.h" -#include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ModUpdateDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" - -#include "DesktopServices.h" +#include "ui/dialogs/ResourceUpdateDialog.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFilterData.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" -#include "modplatform/ModIndex.h" -#include "modplatform/ResourceAPI.h" - -#include "Version.h" #include "tasks/ConcurrentTask.h" #include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" -ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ExternalResourcesPage(inst, mods, parent), m_model(mods) +ModFolderPage::ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent) + : ExternalResourcesPage(inst, model, parent), m_model(model) { - // This is structured like that so that these changes - // do not affect the Resource pack and Shader pack tabs - { - ui->actionDownloadItem->setText(tr("Download mods")); - ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); - ui->actionDownloadItem->setEnabled(true); - ui->actionAddItem->setText(tr("Add file")); - ui->actionAddItem->setToolTip(tr("Add a locally downloaded file")); - - ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - - connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); - - // update menu - auto updateMenu = ui->actionUpdateItem->menu(); - if (updateMenu) { - updateMenu->clear(); - } else { - updateMenu = new QMenu(this); - } + ui->actionDownloadItem->setText(tr("Download Mods")); + ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); + ui->actionDownloadItem->setEnabled(true); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - auto update = updateMenu->addAction(tr("Check for Updates")); - update->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); - connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); - - auto updateWithDeps = updateMenu->addAction(tr("Verify Dependencies")); - updateWithDeps->setToolTip( - tr("Try to update and check for missing dependencies all selected mods (all mods if none are selected)")); - connect(updateWithDeps, &QAction::triggered, this, [this] { updateMods(true); }); - - auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); - updateWithDeps->setVisible(!depsDisabled->get().toBool()); - connect(depsDisabled.get(), &Setting::SettingChanged, this, - [updateWithDeps](const Setting& setting, QVariant value) { updateWithDeps->setVisible(!value.toBool()); }); - - auto actionRemoveItemMetadata = updateMenu->addAction(tr("Reset update metadata")); - actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); - connect(actionRemoveItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); - actionRemoveItemMetadata->setEnabled(false); - - ui->actionUpdateItem->setMenu(updateMenu); - - ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); - connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); - ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); - - ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); - ui->actionsToolbar->addAction(ui->actionVisitItemPage); - connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); - - auto changeVersion = new QAction(tr("Change Version"), this); - changeVersion->setToolTip(tr("Change mod version")); - changeVersion->setEnabled(false); - ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, changeVersion); - connect(changeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion); - - ui->actionsToolbar->insertActionAfter(ui->actionVisitItemPage, ui->actionExportMetadata); - connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); - - auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; - - connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this, check_allow_update, actionRemoveItemMetadata, changeVersion] { - ui->actionUpdateItem->setEnabled(check_allow_update()); - - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); - auto selected = std::count_if(mods_list.cbegin(), mods_list.cend(), - [](Mod* v) { return v->metadata() != nullptr || v->homeurl().size() != 0; }); - if (selected <= 1) { - ui->actionVisitItemPage->setText(tr("Visit mod's page")); - ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); - } else { - ui->actionVisitItemPage->setText(tr("Visit mods' pages")); - ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); - } - - changeVersion->setEnabled(mods_list.length() == 1 && mods_list[0]->metadata() != nullptr); - ui->actionVisitItemPage->setEnabled(selected != 0); - actionRemoveItemMetadata->setEnabled(selected != 0); - }); - - auto updateButtons = [this, check_allow_update] { ui->actionUpdateItem->setEnabled(check_allow_update()); }; - connect(mods.get(), &ModFolderModel::rowsInserted, this, updateButtons); - - connect(mods.get(), &ModFolderModel::rowsRemoved, this, updateButtons); - - connect(mods.get(), &ModFolderModel::updateFinished, this, updateButtons); - } + connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::downloadMods); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(tr("Check for Updates")); + connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); + + updateMenu->addAction(ui->actionVerifyItemDependencies); + connect(ui->actionVerifyItemDependencies, &QAction::triggered, this, [this] { updateMods(true); }); + + 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()); }); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); + + ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected mods.")); + + ui->actionExportMetadata->setToolTip(tr("Export mod's metadata to text.")); + connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); + ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata); + + ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs); } bool ModFolderPage::shouldDisplay() const @@ -170,15 +116,12 @@ bool ModFolderPage::shouldDisplay() const return true; } -bool ModFolderPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - Mod const* m = m_model->at(row); - if (m) - ui->frame->updateWithMod(*m); - - return true; + const Mod& mod = m_model->at(row); + ui->frame->updateWithMod(mod); } void ModFolderPage::removeItems(const QItemSelection& selection) @@ -193,23 +136,32 @@ void ModFolderPage::removeItems(const QItemSelection& selection) if (response != QMessageBox::Yes) return; } - m_model->deleteMods(selection.indexes()); + m_model->deleteResources(selection.indexes()); } -void ModFolderPage::installMods() +void ModFolderPage::downloadMods() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); - return; + if (handleNoModLoader()) { + return; + } } - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ModFolderPage::downloadDialogFinished(int result) +{ + if (result) { + auto tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -226,8 +178,12 @@ void ModFolderPage::installMods() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); @@ -236,6 +192,8 @@ void ModFolderPage::installMods() m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void ModFolderPage::updateMods(bool includeDeps) @@ -245,8 +203,9 @@ void ModFolderPage::updateMods(bool includeDeps) auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); - return; + if (handleNoModLoader()) { + return; + } } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); @@ -266,12 +225,12 @@ void ModFolderPage::updateMods(bool includeDeps) } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - auto mods_list = m_model->selectedMods(selection); + auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) - mods_list = m_model->allMods(); + mods_list = m_model->allResources(); - ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, profile->getModLoadersList()); update_dialog.checkCandidates(); if (update_dialog.aborted()) { @@ -321,65 +280,6 @@ void ModFolderPage::updateMods(bool includeDeps) } } -CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) -{ - auto mcInst = dynamic_cast(m_instance); - if (mcInst) { - auto version = mcInst->getPackProfile(); - if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { - auto minecraftCmp = version->getComponent("net.minecraft"); - if (!minecraftCmp->m_loaded) { - version->reload(Net::Mode::Offline); - auto update = version->getCurrentTask(); - if (update) { - connect(update.get(), &Task::finished, this, [this] { - if (m_container) { - m_container->refreshContainer(); - } - }); - update->start(); - } - } - } - } -} - -bool CoreModFolderPage::shouldDisplay() const -{ - if (ModFolderPage::shouldDisplay()) { - auto inst = dynamic_cast(m_instance); - if (!inst) - return true; - - auto version = inst->getPackProfile(); - if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) - return false; - auto minecraftCmp = version->getComponent("net.minecraft"); - return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; - } - return false; -} - -NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) - : ModFolderPage(inst, mods, parent) -{} - -bool NilModFolderPage::shouldDisplay() const -{ - return m_model->dir().exists(); -} - -void ModFolderPage::visitModPages() -{ - auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); - for (auto mod : m_model->selectedMods(selection)) { - auto url = mod->metaurl(); - if (!url.isEmpty()) - DesktopServices::openUrl(url); - } -} - void ModFolderPage::deleteModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); @@ -398,7 +298,7 @@ void ModFolderPage::deleteModMetadata() return; } - m_model->deleteModsMetadata(selection); + m_model->deleteMetadata(selection); } void ModFolderPage::changeModVersion() @@ -408,8 +308,9 @@ void ModFolderPage::changeModVersion() auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); - return; + if (handleNoModLoader()) { + return; + } } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); @@ -420,36 +321,12 @@ void ModFolderPage::changeModVersion() if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) return; - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setModMetadata((*mods_list.begin())->metadata()); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); - tasks->deleteLater(); - }); - - for (auto& task : mdownload.getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); - - m_model->update(); - } + m_downloadDialog->setResourceMetadata((*mods_list.begin())->metadata()); + m_downloadDialog->open(); } void ModFolderPage::exportModMetadata() @@ -463,3 +340,81 @@ void ModFolderPage::exportModMetadata() ExportToModListDialog dlg(m_instance->name(), selectedMods, this); dlg.exec(); } + +CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent) +{ + auto mcInst = dynamic_cast(m_instance); + if (mcInst) { + auto version = mcInst->getPackProfile(); + if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { + auto minecraftCmp = version->getComponent("net.minecraft"); + if (!minecraftCmp->m_loaded) { + version->reload(Net::Mode::Offline); + auto update = version->getCurrentTask(); + if (update) { + connect(update.get(), &Task::finished, this, [this] { + if (m_container) { + m_container->refreshContainer(); + } + }); + update->start(); + } + } + } + } +} + +bool CoreModFolderPage::shouldDisplay() const +{ + if (ModFolderPage::shouldDisplay()) { + auto inst = dynamic_cast(m_instance); + if (!inst) + return true; + + auto version = inst->getPackProfile(); + if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) + return false; + auto minecraftCmp = version->getComponent("net.minecraft"); + return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; + } + return false; +} + +NilModFolderPage::NilModFolderPage(BaseInstance* inst, ModFolderModel* mods, QWidget* parent) : ModFolderPage(inst, mods, parent) {} + +bool NilModFolderPage::shouldDisplay() const +{ + return m_model->dir().exists(); +} + +// 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(); + + // 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; + } + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 41b605991..62db9fad8 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -38,49 +38,54 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" class ModFolderPage : public ExternalResourcesPage { Q_OBJECT + inline bool handleNoModLoader(); + public: - explicit ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = nullptr); + explicit ModFolderPage(BaseInstance* inst, ModFolderModel* model, QWidget* parent = nullptr); virtual ~ModFolderPage() = default; void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } virtual QString displayName() const override { return tr("Mods"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("loadermods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } virtual QString id() const override { return "mods"; } virtual QString helpPage() const override { return "Loader-mods"; } virtual bool shouldDisplay() const override; public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; private slots: void removeItems(const QItemSelection& selection) override; - void deleteModMetadata(); - void exportModMetadata(); - void installMods(); + void downloadMods(); + void downloadDialogFinished(int result); void updateMods(bool includeDeps = false); - void visitModPages(); + void deleteModMetadata(); + void exportModMetadata(); 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"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } + virtual QString displayName() const override { return tr("Core Mods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "coremods"; } virtual QString helpPage() const override { return "Core-mods"; } @@ -90,11 +95,11 @@ 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"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "nilmods"; } virtual QString helpPage() const override { return "Nilmods"; } diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h index 3351d25fc..f11e2ad7c 100644 --- a/launcher/ui/pages/instance/NotesPage.h +++ b/launcher/ui/pages/instance/NotesPage.h @@ -37,7 +37,6 @@ #include -#include #include "BaseInstance.h" #include "ui/pages/BasePage.h" @@ -54,9 +53,9 @@ class NotesPage : public QWidget, public BasePage { virtual QString displayName() const override { return tr("Notes"); } virtual QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("notes"); + auto icon = QIcon::fromTheme("notes"); if (icon.isNull()) - icon = APPLICATION->getThemedIcon("news"); + icon = QIcon::fromTheme("news"); return icon; } virtual QString id() const override { return "notes"; } diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ab5d98289..c76b7d8d5 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -40,23 +40,59 @@ #include #include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" #include #include +#include +#include +#include #include -#include "RecursiveFileSystemWatcher.h" +#include -OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent) - : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), m_watcher(new RecursiveFileSystemWatcher(this)) +OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, BaseInstance* instance, QWidget* parent) + : QWidget(parent) + , m_id(id) + , m_displayName(displayName) + , m_helpPage(helpPage) + , ui(new Ui::OtherLogsPage) + , m_instance(instance) + , m_basePath(instance ? instance->gameRoot() : APPLICATION->dataRoot()) + , m_logSearchPaths(instance ? instance->getLogFileSearchPaths() : QStringList{ "logs" }) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - m_watcher->setMatcher(fileFilter); - m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); + m_proxy = new LogFormatProxyModel(this); + if (m_instance) { + m_model = new LogModel(this); + ui->trackLogCheckbox->hide(); + } else { + m_model = APPLICATION->logModel.get(); + } - connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); - populateSelectLogBox(); + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + if (m_instance) { + m_model->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + } else { + modelStateToUI(); + } + m_proxy->setSourceModel(m_model); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); @@ -75,6 +111,39 @@ OtherLogsPage::~OtherLogsPage() delete ui; } +void OtherLogsPage::modelStateToUI() +{ + if (m_model->wrapLines()) { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void OtherLogsPage::UIToModelState() +{ + if (!m_model) { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + void OtherLogsPage::retranslate() { ui->retranslateUi(this); @@ -82,74 +151,121 @@ void OtherLogsPage::retranslate() void OtherLogsPage::openedImpl() { - m_watcher->enable(); + const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to start watching" << path; + else + qDebug() << "Started watching" << path; + } + + populateSelectLogBox(); } + void OtherLogsPage::closedImpl() { - m_watcher->disable(); + const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to stop watching" << path; + else + qDebug() << "Stopped watching" << path; + } } void OtherLogsPage::populateSelectLogBox() { + const QString prevCurrentFile = m_currentFile; + + ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); - ui->selectLogBox->addItems(m_watcher->files()); - if (m_currentFile.isEmpty()) { - setControlsEnabled(false); - ui->selectLogBox->setCurrentIndex(-1); - } else { - const int index = ui->selectLogBox->findText(m_currentFile); + if (!m_instance) + ui->selectLogBox->addItem("Current logs"); + ui->selectLogBox->addItems(getPaths()); + ui->selectLogBox->blockSignals(false); + + if (!prevCurrentFile.isEmpty()) { + const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { + ui->selectLogBox->blockSignals(true); ui->selectLogBox->setCurrentIndex(index); + ui->selectLogBox->blockSignals(false); setControlsEnabled(true); + // don't refresh file + return; } else { setControlsEnabled(false); } + } else if (!m_instance) { + ui->selectLogBox->setCurrentIndex(0); + setControlsEnabled(true); } + + on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); } void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) { QString file; - if (index != -1) { + if (index > 0 || (index == 0 && m_instance)) { file = ui->selectLogBox->itemText(index); } - if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file))) { + if ((index != 0 || m_instance) && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); } else { m_currentFile = file; - on_btnReload_clicked(); + reload(); setControlsEnabled(true); } } void OtherLogsPage::on_btnReload_clicked() +{ + if (!m_instance && m_currentFile.isEmpty()) { + if (!m_model) + return; + m_model->clear(); + if (m_container) + m_container->refreshContainer(); + } else { + reload(); + } +} + +void OtherLogsPage::reload() { if (m_currentFile.isEmpty()) { - setControlsEnabled(false); + if (m_instance) { + setControlsEnabled(false); + } else { + m_model = APPLICATION->logModel.get(); + m_proxy->setSourceModel(m_model); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); ui->btnReload->setEnabled(true); // allow reload m_currentFile = QString(); QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); } else { - auto setPlainText = [&](const QString& text) { - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - bool conversionOk = false; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } + auto setPlainText = [this](const QString& text) { QTextDocument* doc = ui->text->document(); - doc->setDefaultFont(QFont(fontFamily, fontSize)); + doc->setDefaultFont(m_proxy->getFont()); ui->text->setPlainText(text); }; - auto showTooBig = [&]() { + auto showTooBig = [setPlainText, &file]() { setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " "for large files.") .arg(file.fileName())); @@ -158,28 +274,83 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - QString content; + MessageLevel last = MessageLevel::Unknown; + + auto handleLine = [this, &last](QString line) { + if (line.isEmpty()) + return false; + if (line.back() == '\n') + line = line.remove(line.size() - 1, 1); + MessageLevel level = MessageLevel::Unknown; + + QString lineTemp = line; // don't edit out the time and level for clarity + if (!m_instance) { + level = MessageLevel::takeFromLauncherLine(lineTemp); + } else { + level = LogParser::guessLevel(line, last); + } + + last = level; + m_model->append(level, line); + return m_model->isOverFlow(); + }; + + // Try to determine a level for each line + ui->text->clear(); + ui->text->setModel(nullptr); + if (!m_instance) { + 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())); + } + m_model->clear(); if (file.fileName().endsWith(".gz")) { - QByteArray temp; - if (!GZip::unzip(file.readAll(), temp)) { - setPlainText(tr("The file (%1) is not readable.").arg(file.fileName())); + QString line; + auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { + auto block = d; + int newlineIndex = block.indexOf('\n'); + while (newlineIndex != -1) { + line += QString::fromUtf8(block).left(newlineIndex); + block.remove(0, newlineIndex + 1); + if (handleLine(line)) { + line.clear(); + return false; + } + line.clear(); + newlineIndex = block.indexOf('\n'); + } + line += QString::fromUtf8(block); + return true; + }); + if (!error.isEmpty()) { + setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); return; + } else if (!line.isEmpty()) { + handleLine(line); } - content = QString::fromUtf8(temp); } else { - content = QString::fromUtf8(file.readAll()); + while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { + } } - if (content.size() >= 50000000ll) { - showTooBig(); - return; + + if (m_instance) { + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + } else { + m_proxy->setSourceModel(m_model); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); } - setPlainText(content); } } void OtherLogsPage::on_btnPaste_clicked() { - GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); + QString name = m_currentFile.isEmpty() ? displayName() : m_currentFile; + GuiUtil::uploadPaste(name, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() @@ -187,6 +358,18 @@ void OtherLogsPage::on_btnCopy_clicked() GuiUtil::setClipboardText(ui->text->toPlainText()); } +void OtherLogsPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void OtherLogsPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!m_model) + return; + m_model->suspend(!checked); +} + void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { @@ -201,7 +384,7 @@ void OtherLogsPage::on_btnDelete_clicked() QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (FS::trash(file.fileName())) { return; @@ -214,7 +397,7 @@ void OtherLogsPage::on_btnDelete_clicked() void OtherLogsPage::on_btnClean_clicked() { - auto toDelete = m_watcher->files(); + auto toDelete = getPaths(); if (toDelete.isEmpty()) { return; } @@ -237,7 +420,9 @@ void OtherLogsPage::on_btnClean_clicked() } QStringList failed; for (auto item : toDelete) { - QFile file(FS::PathCombine(m_path, item)); + QString absolutePath = FS::PathCombine(m_basePath, item); + QFile file(absolutePath); + qDebug() << "Deleting log" << absolutePath; if (FS::trash(file.fileName())) { continue; } @@ -263,37 +448,87 @@ void OtherLogsPage::on_btnClean_clicked() } } +void OtherLogsPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!m_model) + return; + m_model->setLineWrap(checked); + ui->text->scrollToBottom(); +} + +void OtherLogsPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); + ui->text->scrollToBottom(); +} + void OtherLogsPage::setControlsEnabled(const bool enabled) { + if (m_instance) { + 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->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + ui->trackLogCheckbox->setEnabled(false); + } else { + ui->btnReload->setText("Clear"); + ui->btnReload->setToolTip("Clear the log"); + ui->btnDelete->setEnabled(false); + ui->btnClean->setEnabled(false); + ui->trackLogCheckbox->setEnabled(enabled); + } + ui->btnReload->setEnabled(enabled); - ui->btnDelete->setEnabled(enabled); ui->btnCopy->setEnabled(enabled); ui->btnPaste->setEnabled(enabled); ui->text->setEnabled(enabled); - ui->btnClean->setEnabled(enabled); } -// FIXME: HACK, use LogView instead? -static void findNext(QPlainTextEdit* _this, const QString& what, bool reverse) +QStringList OtherLogsPage::getPaths() { - _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); + QDir baseDir(m_basePath); + + QStringList result; + + for (QString searchPath : m_logSearchPaths) { + QDir searchDir(searchPath); + + QStringList filters{ "*.log", "*.log.gz" }; + + if (searchPath != m_basePath) + filters.append("*.txt"); + + QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); + + for (const QString& name : entries) + result.append(baseDir.relativeFilePath(searchDir.filePath(name))); + } + + return result; } void OtherLogsPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); bool reverse = modifiers & Qt::ShiftModifier; - findNext(ui->text, ui->searchBar->text(), reverse); + ui->text->findNext(ui->searchBar->text(), reverse); } void OtherLogsPage::findNextActivated() { - findNext(ui->text, ui->searchBar->text(), false); + ui->text->findNext(ui->searchBar->text(), false); } void OtherLogsPage::findPreviousActivated() { - findNext(ui->text, ui->searchBar->text(), true); + ui->text->findNext(ui->searchBar->text(), true); } void OtherLogsPage::findActivated() diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 85a3a2dbc..cd2fe6439 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -38,7 +38,8 @@ #include #include -#include +#include +#include "LogPage.h" #include "ui/pages/BasePage.h" namespace Ui { @@ -51,13 +52,13 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent = 0); + explicit OtherLogsPage(QString id, QString displayName, QString helpPage, BaseInstance* instance = nullptr, QWidget* parent = 0); ~OtherLogsPage(); - QString id() const override { return "logs"; } - QString displayName() const override { return tr("Other logs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString helpPage() const override { return "other-Logs"; } + QString id() const override { return m_id; } + QString displayName() const override { return m_displayName; } + QIcon icon() const override { return QIcon::fromTheme("log"); } + QString helpPage() const override { return m_helpPage; } void retranslate() override; void openedImpl() override; @@ -71,6 +72,11 @@ class OtherLogsPage : public QWidget, public BasePage { void on_btnCopy_clicked(); void on_btnDelete_clicked(); void on_btnClean_clicked(); + void on_btnBottom_clicked(); + + void on_trackLogCheckbox_clicked(bool checked); + void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); @@ -78,12 +84,26 @@ class OtherLogsPage : public QWidget, public BasePage { void findPreviousActivated(); private: + void reload(); + void modelStateToUI(); + void UIToModelState(); void setControlsEnabled(bool enabled); + QStringList getPaths(); + private: + QString m_id; + QString m_displayName; + QString m_helpPage; + Ui::OtherLogsPage* ui; - QString m_path; + BaseInstance* m_instance; + /** Path to display log paths relative to. */ + QString m_basePath; + QStringList m_logSearchPaths; QString m_currentFile; - IPathMatcher::Ptr m_fileFilter; - RecursiveFileSystemWatcher* m_watcher; + QFileSystemWatcher m_watcher; + + LogFormatProxyModel* m_proxy; + LogModel* m_model; }; diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 3fdb023fe..77076d4ab 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -10,7 +10,7 @@ 538 - + 0 @@ -18,126 +18,209 @@ 0 - 0 + 6 0 - - - - 0 + + + + + 0 + 0 + + + + &Find + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + Scroll all the way to bottom + + + &Bottom + + + + + + + false + + + false - - - Tab 1 - - - - + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + + + + 0 + 0 + + + + + + + + Delete the selected log + + + &Delete Selected + + + + + + + Delete all the logs + + + Delete &All + + + + + + + + + + + Keep updating + + + true + + - - + + - Find + Wrap lines + + + true - - - - false + + + + Color lines - + true - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Clear the log - - - Delete - - - - - - - Upload the log to the paste service configured in preferences. - - - Upload - - - - - - - Clear the log - - - Clean - - - - - - - Reload - - - - - - - - 0 - 0 - - - - - + + + + Upload the log to the paste service configured in preferences + + + &Upload + + - - + + + + Reload the contents of the log from the disk + - Search: + &Reload - + + + + + + + Search + + + + LogView + QPlainTextEdit +
    ui/widgets/LogView.h
    +
    +
    - tabWidget selectLogBox btnReload btnCopy btnPaste btnDelete btnClean + wrapCheckbox + colorCheckbox text searchBar findButton diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 054bede01..eb085e29b 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -40,36 +40,60 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" -ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) +ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, ResourcePackFolderModel* model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download packs")); - ui->actionDownloadItem->setToolTip(tr("Download resource packs from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download resource packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadRPs); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadResourcePacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected resource packs (all resource packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ResourcePackPage::deleteResourcePackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ResourcePackPage::changeResourcePackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } -bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void ResourcePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithResourcePack(rp); - - return true; } -void ResourcePackPage::downloadRPs() +void ResourcePackPage::downloadResourcePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ResourcePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); - if (mdownload.exec()) { + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ResourcePackPage::downloadDialogFinished(int result) +{ + if (result) { auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -87,7 +111,91 @@ void ResourcePackPage::downloadRPs() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +void ResourcePackPage::updateResourcePacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable( + this, tr("Confirm Update"), + tr("Updating resource packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The resource pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All resource packs are up-to-date! :)"); + } else { + message = tr("All selected resource packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } @@ -98,3 +206,52 @@ void ResourcePackPage::downloadRPs() m_model->update(); } } + +void ResourcePackPage::deleteResourcePackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedResourcePacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 resource packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void ResourcePackPage::changeResourcePackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index cb84ca96d..4e673e98c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/ResourcePackFolderModel.h" @@ -45,10 +48,10 @@ 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 APPLICATION->getThemedIcon("resourcepacks"); } + QString displayName() const override { return tr("Resource Packs"); } + QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "resourcepacks"; } QString helpPage() const override { return "Resource-packs"; } @@ -58,6 +61,16 @@ class ResourcePackPage : public ExternalResourcesPage { } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; - void downloadRPs(); + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + + private slots: + void downloadResourcePacks(); + void downloadDialogFinished(int result); + void updateResourcePacks(); + void deleteResourcePackMetadata(); + void changeResourcePackVersion(); + + protected: + ResourcePackFolderModel* m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index b619a07b8..9aa7ea50a 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -134,8 +134,8 @@ class FilterModel : public QIdentityProxyModel { { m_thumbnailingPool.setMaxThreadCount(4); m_thumbnailCache = std::make_shared(); - m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); - connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder")); + connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged); } virtual ~FilterModel() { @@ -150,7 +150,8 @@ class FilterModel : public QIdentityProxyModel { return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); - return result.toString().remove(QRegularExpression("\\.png$")); + static const QRegularExpression s_removeChars("\\.png$"); + return result.toString().remove(s_removeChars); } if (role == Qt::DecorationRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); @@ -190,9 +191,9 @@ class FilterModel : public QIdentityProxyModel { void thumbnailImage(QString path) { auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); - connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)), SLOT(thumbnailReady(QString))); - connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)), SLOT(thumbnailFailed(QString))); - ((QThreadPool&)m_thumbnailingPool).start(runnable); + 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(); } @@ -265,7 +266,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); - connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex))); + connect(ui->listView, &QAbstractItemView::activated, this, &ScreenshotsPage::onItemActivated); } bool ScreenshotsPage::eventFilter(QObject* obj, QEvent* evt) @@ -559,17 +560,14 @@ void ScreenshotsPage::openedImpl() } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ScreenshotsPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } #include "ScreenshotsPage.moc" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index bb127b429..b9c750a1f 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -37,7 +37,6 @@ #include -#include #include "ui/pages/BasePage.h" #include "settings/Setting.h" @@ -67,7 +66,7 @@ class ScreenshotsPage : public QMainWindow, public BasePage { virtual bool eventFilter(QObject*, QEvent*) override; virtual QString displayName() const override { return tr("Screenshots"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("screenshots"); } + virtual QIcon icon() const override { return QIcon::fromTheme("screenshots"); } virtual QString id() const override { return "screenshots"; } virtual QString helpPage() const override { return "Screenshots-management"; } virtual bool apply() override { return !m_uploadActive; } diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui index 2e2227a29..db55869cd 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.ui +++ b/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -26,11 +26,17 @@ + + false + - QAbstractItemView::ExtendedSelection + QAbstractItemView::SelectionMode::ExtendedSelection - QAbstractItemView::SelectRows + QAbstractItemView::SelectionBehavior::SelectRows + + + QListView::Movement::Static @@ -41,7 +47,7 @@ Actions - Qt::ToolButtonTextOnly + Qt::ToolButtonStyle::ToolButtonTextOnly RightToolBarArea diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp new file mode 100644 index 000000000..98c9060b2 --- /dev/null +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include "Exception.h" +#include "McClient.h" +#include "McResolver.h" +#include "ServerPingTask.h" + +unsigned getOnlinePlayers(QJsonObject data) +{ + try { + return Json::requireInteger(Json::requireObject(data, "players"), "online"); + } catch (Exception& e) { + qWarning() << "server ping failed to parse response" << e.what(); + return 0; + } +} + +void ServerPingTask::executeTask() +{ + qDebug() << "Querying status of " << QString("%1:%2").arg(m_domain).arg(m_port); + + // Resolve the actual IP and port for the server + McResolver* resolver = new McResolver(nullptr, m_domain, m_port); + connect(resolver, &McResolver::succeeded, this, [this](QString ip, int port) { + qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port; + + // Now that we have the IP and port, query the server + McClient* client = new McClient(nullptr, m_domain, ip, port); + + connect(client, &McClient::succeeded, this, [this](QJsonObject data) { + m_outputOnlinePlayers = getOnlinePlayers(data); + qDebug() << "Online players: " << m_outputOnlinePlayers; + emitSucceeded(); + }); + connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); + + // Delete McClient object when done + connect(client, &McClient::finished, this, [client]() { client->deleteLater(); }); + client->getStatusData(); + }); + connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); + + // Delete McResolver object when done + connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); + resolver->ping(); +} diff --git a/launcher/ui/pages/instance/ServerPingTask.h b/launcher/ui/pages/instance/ServerPingTask.h new file mode 100644 index 000000000..6f03b92ad --- /dev/null +++ b/launcher/ui/pages/instance/ServerPingTask.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include + +class ServerPingTask : public Task { + Q_OBJECT + public: + explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {} + ~ServerPingTask() override = default; + int m_outputOnlinePlayers = -1; + + private: + QString m_domain; + int m_port; + + protected: + virtual void executeTask() override; +}; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d8035e73e..206658f63 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -36,6 +36,8 @@ */ #include "ServersPage.h" +#include "Application.h" +#include "ServerPingTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" @@ -48,11 +50,12 @@ #include #include +#include #include #include #include -static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. +static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things. struct Server { // Types @@ -109,12 +112,7 @@ struct Server { QByteArray m_icon; // Data - temporary - bool m_checked = false; - bool m_up = false; - QString m_motd; // https://mctools.org/motd-creator - int m_ping = 0; - int m_currentPlayers = 0; - int m_maxPlayers = 0; + std::optional m_currentPlayers; // nullopt if not calculated/calculating }; static std::unique_ptr parseServersDat(const QString& filename) @@ -254,11 +252,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row - 1, row); -#else - m_servers.swap(row - 1, row); -#endif endMoveRows(); scheduleSave(); return true; @@ -274,11 +268,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row + 1, row); -#else - m_servers.swap(row + 1, row); -#endif endMoveRows(); scheduleSave(); return true; @@ -296,7 +286,7 @@ class ServersModel : public QAbstractListModel { case 1: return tr("Address"); case 2: - return tr("Latency"); + return tr("Online"); } } @@ -316,39 +306,40 @@ class ServersModel : public QAbstractListModel { if (row < 0 || row >= m_servers.size()) return QVariant(); - switch (column) { - case 0: - switch (role) { - case Qt::DecorationRole: { - auto& bytes = m_servers[row].m_icon; - if (bytes.size()) { - QPixmap px; - if (px.loadFromData(bytes)) - return QIcon(px); - } - return APPLICATION->getThemedIcon("unknown_server"); + switch (role) { + case Qt::DecorationRole: { + if (column == 0) { + auto& bytes = m_servers[row].m_icon; + if (bytes.size()) { + QPixmap px; + if (px.loadFromData(bytes)) + return QIcon(px); } - case Qt::DisplayRole: - return m_servers[row].m_name; - case ServerPtrRole: - return QVariant::fromValue((void*)&m_servers[row]); - default: - return QVariant(); + return QIcon::fromTheme("unknown_server"); + } else { + return QVariant(); } - case 1: - switch (role) { - case Qt::DisplayRole: + } + case Qt::DisplayRole: + switch (column) { + case 0: + return m_servers[row].m_name; + case 1: return m_servers[row].m_address; + case 2: + if (m_servers[row].m_currentPlayers) { + return *m_servers[row].m_currentPlayers; + } else { + return "..."; + } default: return QVariant(); } - case 2: - switch (role) { - case Qt::DisplayRole: - return m_servers[row].m_ping; - default: - return QVariant(); - } + case ServerPtrRole: + if (column == 0) + return QVariant::fromValue((void*)&m_servers[row]); + else + return QVariant(); default: return QVariant(); } @@ -433,6 +424,40 @@ class ServersModel : public QAbstractListModel { } } + void queryServersStatus() + { + // Abort the currently running task if present + if (m_currentQueryTask != nullptr) { + m_currentQueryTask->abort(); + qDebug() << "Aborted previous server query task"; + } + + m_currentQueryTask = ConcurrentTask::Ptr( + new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + int row = 0; + for (Server& server : m_servers) { + // reset current players + server.m_currentPlayers = {}; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + + // Start task to query server status + auto target = MinecraftTarget::parse(server.m_address, false); + auto* task = new ServerPingTask(target.address, target.port); + m_currentQueryTask->addTask(Task::Ptr(task)); + + // Update the model when the task is done + connect(task, &Task::finished, this, [this, task, row]() { + if (m_servers.size() < row) + return; + m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + }); + row++; + } + + m_currentQueryTask->start(); + } + public slots: void dirChanged(const QString& path) { @@ -520,9 +545,10 @@ class ServersModel : public QAbstractListModel { QList m_servers; QFileSystemWatcher* m_watcher = nullptr; QTimer m_saveTimer; + ConcurrentTask::Ptr m_currentQueryTask = nullptr; }; -ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) +ServersPage::ServersPage(BaseInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) { ui->setupUi(this); m_inst = inst; @@ -542,10 +568,10 @@ 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, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(ui->resourceComboBox, &QComboBox::currentIndexChanged, this, &ServersPage::resourceIndexChanged); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); m_locked = m_inst->isRunning(); @@ -670,19 +696,19 @@ void ServersPage::openedImpl() m_model->observe(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); + + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + // ping servers + m_model->queryServersStatus(); } void ServersPage::closedImpl() { m_model->unobserve(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } void ServersPage::on_actionAdd_triggered() @@ -734,4 +760,9 @@ void ServersPage::on_actionJoin_triggered() APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } +void ServersPage::on_actionRefresh_triggered() +{ + m_model->queryServersStatus(); +} + #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index a27d1d297..0eec57de1 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -39,7 +39,7 @@ #include #include -#include +#include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "settings/Setting.h" @@ -56,14 +56,14 @@ 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; void closedImpl() override; virtual QString displayName() const override { return tr("Servers"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("server"); } + virtual QIcon icon() const override { return QIcon::fromTheme("server"); } virtual QString id() const override { return "servers"; } virtual QString helpPage() const override { return "Servers-management"; } void retranslate() override; @@ -85,6 +85,7 @@ class ServersPage : public QMainWindow, public BasePage { void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); + void on_actionRefresh_triggered(); void runningStateChanged(bool running); @@ -99,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 e8f79cf2e..d330835c8 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -149,6 +149,8 @@ + + @@ -175,6 +177,11 @@ Join + + + Refresh + + diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 61ee01445..3120d9013 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -45,27 +45,53 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" -ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) +ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, ShaderPackFolderModel* model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download shaders")); - ui->actionDownloadItem->setToolTip(tr("Download shaders from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download shader packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaders); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaderPack); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected shader packs (all shader packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ShaderPackPage::deleteShaderPackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a shader pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &ShaderPackPage::changeShaderPackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } -void ShaderPackPage::downloadShaders() +void ShaderPackPage::downloadShaderPack() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ShaderPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask("Download Shaders", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void ShaderPackPage::downloadDialogFinished(int result) +{ + if (result) { + auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -82,7 +108,91 @@ void ShaderPackPage::downloadShaders() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +void ShaderPackPage::updateShaderPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(this, tr("Confirm Update"), + tr("Updating shader packs while the game is running may pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The shader pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All shader packs are up-to-date! :)"); + } else { + message = tr("All selected shader packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } @@ -93,3 +203,52 @@ void ShaderPackPage::downloadShaders() m_model->update(); } } + +void ShaderPackPage::deleteShaderPackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedShaderPacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 shader packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void ShaderPackPage::changeShaderPackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index d134e67ad..cc53a01e1 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -37,21 +37,31 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" 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"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } + QString displayName() const override { return tr("Shader Packs"); } + QIcon icon() const override { return QIcon::fromTheme("shaderpacks"); } QString id() const override { return "shaderpacks"; } QString helpPage() const override { return "shader-packs"; } bool shouldDisplay() const override { return true; } public slots: - void downloadShaders(); + void downloadShaderPack(); + void downloadDialogFinished(int result); + void updateShaderPacks(); + void deleteShaderPackMetadata(); + void changeShaderPackVersion(); + + private: + ShaderPackFolderModel* m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 9c8ee3c78..ec0486fe4 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -44,36 +44,61 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" -TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) +TexturePackPage::TexturePackPage(MinecraftInstance* instance, TexturePackFolderModel* model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download packs")); - ui->actionDownloadItem->setToolTip(tr("Download texture packs from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download texture packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTPs); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTexturePacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected texture packs (all texture packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &TexturePackPage::deleteTexturePackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a texture pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &TexturePackPage::changeTexturePackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); + + ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected texture packs.")); } -bool TexturePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +void TexturePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithTexturePack(rp); - - return true; } -void TexturePackPage::downloadTPs() +void TexturePackPage::downloadTexturePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::TexturePackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); - if (mdownload.exec()) { + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); + m_downloadDialog->open(); +} + +void TexturePackPage::downloadDialogFinished(int result) +{ + if (result) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -91,7 +116,91 @@ void TexturePackPage::downloadTPs() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +void TexturePackPage::updateTexturePacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable( + this, tr("Confirm Update"), + tr("Updating texture packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The texture pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All texture packs are up-to-date! :)"); + } else { + message = tr("All selected texture packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } @@ -102,3 +211,52 @@ void TexturePackPage::downloadTPs() m_model->update(); } } + +void TexturePackPage::deleteTexturePackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedTexturePacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 texture packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void TexturePackPage::changeTexturePackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); + + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); +} diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 9c4f24b70..dad0affa4 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" @@ -45,16 +48,24 @@ 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 APPLICATION->getThemedIcon("resourcepacks"); } + QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "texturepacks"; } QString helpPage() const override { return "Texture-packs"; } virtual bool shouldDisplay() const override { return m_instance->traits().contains("texturepacks"); } public slots: - bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; - void downloadTPs(); + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + void downloadTexturePacks(); + void downloadDialogFinished(int result); + void updateTexturePacks(); + void deleteTexturePackMetadata(); + void changeTexturePackVersion(); + + private: + TexturePackFolderModel* m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 975c44de2..fea759bb2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -91,12 +91,12 @@ class IconProxy : public QIdentityProxyModel { if (!var.isNull()) { auto string = var.toString(); if (string == "warning") { - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); } else if (string == "error") { - return APPLICATION->getThemedIcon("status-bad"); + return QIcon::fromTheme("status-bad"); } } - return APPLICATION->getThemedIcon("status-good"); + return QIcon::fromTheme("status-good"); } return var; } @@ -124,16 +124,13 @@ void VersionPage::retranslate() void VersionPage::openedImpl() { auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void VersionPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } QMenu* VersionPage::createPopupMenu() @@ -154,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); @@ -171,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/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 9be21d499..d525f56a5 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -43,18 +43,11 @@
    - - - - - - - - Filter: - - - - + + + Search + + diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 4ed5f1f73..ecb48accb 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -40,12 +40,15 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -57,6 +60,7 @@ #include "ui/GuiUtil.h" #include "Application.h" +#include "DataPackPage.h" class WorldListProxyModel : public QSortFilterProxyModel { Q_OBJECT @@ -73,7 +77,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); if (iconFile.isNull()) { // NOTE: Minecraft uses the same placeholder for servers AND worlds - return APPLICATION->getThemedIcon("unknown_server"); + return QIcon::fromTheme("unknown_server"); } return QIcon(iconFile); } @@ -82,7 +86,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(InstancePtr 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); @@ -91,7 +95,7 @@ WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds WorldListProxyModel* proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); - proxy->setSourceModel(m_worlds.get()); + proxy->setSourceModel(m_worlds); proxy->setSortRole(Qt::UserRole); ui->worldTreeView->setSortingEnabled(true); ui->worldTreeView->setModel(proxy); @@ -113,25 +117,21 @@ 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()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void WorldListPage::closedImpl() { m_worlds->stopWatching(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } WorldListPage::~WorldListPage() @@ -166,12 +166,9 @@ void WorldListPage::retranslate() bool WorldListPage::worldListFilter(QKeyEvent* keyEvent) { - switch (keyEvent->key()) { - case Qt::Key_Delete: - on_actionRemove_triggered(); - return true; - default: - break; + if (keyEvent->key() == Qt::Key_Delete) { + on_actionRemove_triggered(); + return true; } return QWidget::eventFilter(ui->worldTreeView, keyEvent); } @@ -215,7 +212,7 @@ void WorldListPage::on_actionView_Folder_triggered() DesktopServices::openPath(m_worlds->dir().absolutePath(), true); } -void WorldListPage::on_actionDatapacks_triggered() +void WorldListPage::on_actionData_Packs_triggered() { QModelIndex index = getSelectedWorld(); @@ -223,12 +220,48 @@ void WorldListPage::on_actionDatapacks_triggered() return; } - if (!worldSafetyNagQuestion(tr("Open World Datapacks Folder"))) + if (!worldSafetyNagQuestion(tr("Manage Data Packs"))) return; - auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString folder = FS::PathCombine(fullPath, "datapacks"); + + auto dialog = new QDialog(this); + dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString())); + dialog->setWindowModality(Qt::WindowModal); + + dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), + static_cast(std::max(0.75 * window()->height(), 400.0))); + dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); + + GenericPageProvider provider(dialog->windowTitle()); + + 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); + + auto focusStealer = new QPushButton(dialog); + layout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); + + auto pageContainer = new PageContainer(&provider, {}, dialog); + pageContainer->hidePageList(); + layout->addWidget(pageContainer); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); + layout->addWidget(buttonBox); + + dialog->setLayout(layout); + + dialog->exec(); - DesktopServices::openPath(FS::PathCombine(fullPath, "datapacks"), true); + APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); } void WorldListPage::on_actionReset_Icon_triggered() @@ -341,12 +374,11 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); - ui->actionDatapacks->setEnabled(enable); + ui->actionData_Packs->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); - auto mInst = std::dynamic_pointer_cast(m_inst); - auto supportsJoin = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + auto supportsJoin = m_inst && m_inst->traits().contains("feature:is_quick_play_singleplayer"); ui->actionJoin->setEnabled(enable && supportsJoin); if (!supportsJoin) { diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 84d9cd075..0afb9883b 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -37,7 +37,6 @@ #include -#include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" @@ -53,11 +52,11 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(InstancePtr 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"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("worlds"); } + virtual QIcon icon() const override { return QIcon::fromTheme("worlds"); } virtual QString id() const override { return "worlds"; } virtual QString helpPage() const override { return "Worlds"; } virtual bool shouldDisplay() const override; @@ -72,7 +71,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - InstancePtr m_inst; + MinecraftInstance* m_inst; private: QModelIndex getSelectedWorld(); @@ -82,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(); @@ -97,7 +97,7 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionRename_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); - void on_actionDatapacks_triggered(); + void on_actionData_Packs_triggered(); void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 04344b453..22c93256c 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -86,7 +86,7 @@ - + @@ -146,12 +146,12 @@ Remove world icon to make the game re-generate it on next load. - + - Datapacks + Data Packs - Manage datapacks inside the world. + Manage data packs inside the world. diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index ba22bd2e6..87e126fd7 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -104,7 +104,7 @@ void CustomPage::filterChanged() if (ui->experimentsFilter->isChecked()) out << "(experiment)"; auto regexp = out.join('|'); - ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); + ui->versionList->setFilter(BaseVersionList::TypeRole, Filters::regexp(QRegularExpression(regexp))); } void CustomPage::loaderFilterChanged() diff --git a/launcher/ui/pages/modplatform/CustomPage.h b/launcher/ui/pages/modplatform/CustomPage.h index c5d6d5af5..2bfb1de29 100644 --- a/launcher/ui/pages/modplatform/CustomPage.h +++ b/launcher/ui/pages/modplatform/CustomPage.h @@ -37,7 +37,7 @@ #include -#include +#include "BaseVersion.h" #include "tasks/Task.h" #include "ui/pages/BasePage.h" @@ -54,7 +54,7 @@ class CustomPage : public QWidget, public BasePage { explicit CustomPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~CustomPage(); virtual QString displayName() const override { return tr("Custom"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("minecraft"); } + virtual QIcon icon() const override { return QIcon::fromTheme("minecraft"); } virtual QString id() const override { return "vanilla"; } virtual QString helpPage() const override { return "Vanilla-platform"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp new file mode 100644 index 000000000..fb535ae03 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackModel.h" + +#include + +namespace ResourceDownload { + +DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) +{} + +/******** Make data requests ********/ + +ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() +{ + auto sort = getCurrentSortingMethodByIndex(); + return { ModPlatform::ResourceType::DataPack, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; +} + +ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) +{ + auto pack = m_packs[entry.row()]; + return { pack, {}, ModPlatform::ModLoaderType::DataPack }; +} + +ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry) +{ + auto pack = m_packs[entry.row()]; + return { pack }; +} + +void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) +{ + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { + return; + } + + setSearchTerm(term); + m_current_sort_index = sort; + + refresh(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h new file mode 100644 index 000000000..29b11ffd6 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +#include "BaseInstance.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourceModel.h" + +class Version; + +namespace ResourceDownload { + +class DataPackResourceModel : public ResourceModel { + Q_OBJECT + + public: + DataPackResourceModel(BaseInstance const&, ResourceAPI*, QString, QString); + + /* Ask the API for more information */ + void searchWithTerm(const QString& term, unsigned int sort); + + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } + + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; + + protected: + const BaseInstance& m_base_instance; + + private: + QString m_debugName; + QString m_metaEntryBase; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.cpp b/launcher/ui/pages/modplatform/DataPackPage.cpp new file mode 100644 index 000000000..82892b318 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackPage.h" +#include "ui_ResourcePage.h" + +#include "DataPackModel.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include + +namespace ResourceDownload { + +DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void DataPackResourcePage::triggerSearch() +{ + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + + updateSelectionButton(); + + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); +} + +QMap DataPackResourcePage::urlHandlers() const +{ + QMap map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), + "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h new file mode 100644 index 000000000..431fc9a05 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/DataPackModel.h" +#include "ui/pages/modplatform/ResourcePage.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class DataPackDownloadDialog; + +class DataPackResourcePage : public ResourcePage { + Q_OBJECT + + public: + template + static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); + + return page; + } + + //: The plural version of 'data pack' + inline QString resourcesString() const override { return tr("data packs"); } + //: The singular version of 'data packs' + inline QString resourceString() const override { return tr("data pack"); } + + bool supportsFiltering() const override { return false; }; + + QMap urlHandlers() const override; + + protected: + DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance); + + protected slots: + void triggerSearch() override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 1efc6199e..a345f0d41 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -134,20 +134,20 @@ void ImportPage::updateState() auto array = std::make_shared(); auto api = FlameAPI(); - auto job = api.getFile(addonId, fileId, array); + auto job = api.getFile(addonId, fileId, array.get()); connect(job.get(), &NetJob::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] { qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); auto doc = Json::requireDocument(*array); - auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); + auto data = doc.object()["data"].toObject(); // No way to find out if it's a mod or a modpack before here // And also we need to check if it ends with .zip, instead of any better way - auto fileName = Json::ensureString(data, "fileName"); + auto fileName = data["fileName"].toString(); if (fileName.endsWith(".zip")) { // Have to use ensureString then use QUrl to get proper url encoding - auto dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl")); + auto dl_url = QUrl(data["downloadUrl"].toString("")); if (!dl_url.isValid()) { CustomMessageBox::selectable( this, tr("Error"), @@ -158,7 +158,7 @@ void ImportPage::updateState() } QFileInfo dl_file(dl_url.fileName()); - QString pack_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName"); + QString pack_name = data["displayName"].toString(dl_file.completeBaseName()); QMap extra_info; extra_info.insert("pack_id", addonId); diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index 70d7736eb..1119e709a 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -37,7 +37,6 @@ #include -#include #include "tasks/Task.h" #include "ui/pages/BasePage.h" @@ -54,7 +53,7 @@ class ImportPage : public QWidget, public BasePage { explicit ImportPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportPage(); virtual QString displayName() const override { return tr("Import"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("viewfolder"); } + virtual QIcon icon() const override { return QIcon::fromTheme("viewfolder"); } virtual QString id() const override { return "import"; } virtual QString helpPage() const override { return "Zip-import"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 1f0329321..ef38a595d 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -7,13 +7,16 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" #include #include namespace ResourceDownload { -ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} +ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) +{} /******** Make data requests ********/ @@ -39,12 +42,14 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories }; + return { + ModPlatform::ResourceType::Mod, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories, m_filter->openSource + }; } -ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry) { - auto& pack = *m_packs[entry.row()]; + auto pack = m_packs[entry.row()]; auto profile = static_cast(m_base_instance).getPackProfile(); Q_ASSERT(profile); @@ -57,12 +62,12 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en if (m_filter->loaders) loaders = m_filter->loaders; - return { pack, versions, loaders }; + return { pack, versions, loaders, ModPlatform::ResourceType::Mod }; } -ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) { - auto& pack = *m_packs[entry.row()]; + auto pack = m_packs[entry.row()]; return { pack }; } @@ -99,9 +104,10 @@ QVariant ModModel::getInstalledPackVersion(ModPlatform::IndexedPack::Ptr pack) c return {}; } -bool checkSide(QString filter, QString value) +bool checkSide(ModPlatform::Side filter, ModPlatform::Side value) { - return filter.isEmpty() || value.isEmpty() || filter == "both" || value == "both" || filter == value; + return (filter != ModPlatform::Side::ClientSide && filter != ModPlatform::Side::ServerSide) || + (value != ModPlatform::Side::ClientSide && value != ModPlatform::Side::ServerSide) || filter == value; } bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack) diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 5c994f373..873d4c1f9 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -24,26 +24,23 @@ class ModModel : public ResourceModel { Q_OBJECT public: - ModModel(BaseInstance&, ResourceAPI* api); + ModModel(BaseInstance&, ResourceAPI* api, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; - virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; - void setFilter(std::shared_ptr filter) { m_filter = filter; } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } + public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override; @@ -53,6 +50,10 @@ class ModModel : public ResourceModel { BaseInstance& m_base_instance; std::shared_ptr m_filter = nullptr; + + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f0cc2df54..706d35378 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -59,10 +59,9 @@ namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); - connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } -void ModPage::setFilterWidget(unique_qobject_ptr& widget) +void ModPage::setFilterWidget(std::unique_ptr& widget) { if (m_filter_widget) disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); @@ -113,9 +112,7 @@ QMap ModPage::urlHandlers() const /******** Make changes to the UI ********/ -void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, - ModPlatform::IndexedVersion& version, - const std::shared_ptr base_model) +void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, ResourceFolderModel* base_model) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_model->addPack(pack, version, base_model, is_indexed); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5c9a82303..a69ee53f7 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -35,26 +35,27 @@ class ModPage : public ResourcePage { page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'mod' - [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); } + inline QString resourcesString() const override { return tr("mods"); } //: The singular version of 'mods' - [[nodiscard]] inline QString resourceString() const override { return tr("mod"); } + inline QString resourceString() const override { return tr("mod"); } - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; - void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; + void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, ResourceFolderModel*) override; - virtual unique_qobject_ptr createFilterWidget() = 0; + virtual std::unique_ptr createFilterWidget() = 0; - [[nodiscard]] bool supportsFiltering() const override { return true; }; + bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - void setFilterWidget(unique_qobject_ptr&); + void setFilterWidget(std::unique_ptr&); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); @@ -66,7 +67,7 @@ class ModPage : public ResourcePage { void triggerSearch() override; protected: - unique_qobject_ptr m_filter_widget; + std::unique_ptr m_filter_widget; std::shared_ptr m_filter; }; diff --git a/launcher/ui/pages/modplatform/ModpackProviderBasePage.h b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h index a3daa9a81..03faebaf5 100644 --- a/launcher/ui/pages/modplatform/ModpackProviderBasePage.h +++ b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h @@ -25,5 +25,5 @@ class ModpackProviderBasePage : public BasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) = 0; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const = 0; -}; \ No newline at end of file + virtual QString getSerachTerm() const = 0; +}; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 50a170fff..036a50467 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -15,8 +15,8 @@ #include "Application.h" #include "BuildConfig.h" -#include "Json.h" +#include "modplatform/ResourceAPI.h" #include "net/ApiDownload.h" #include "net/NetJob.h" @@ -65,7 +65,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant icon_or_none.has_value()) return icon_or_none.value(); - return APPLICATION->getThemedIcon("screenshot-placeholder"); + return QIcon::fromTheme("screenshot-placeholder"); } else { return {}; } @@ -82,8 +82,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; - case UserDataTypes::SELECTED: - return pack->isAnyVersionSelected(); + case Qt::CheckStateRole: + return pack->isAnyVersionSelected() ? Qt::Checked : Qt::Unchecked; case UserDataTypes::INSTALLED: return this->isPackInstalled(pack); default: @@ -103,7 +103,6 @@ QHash ResourceModel::roleNames() const roles[Qt::UserRole] = "pack"; roles[UserDataTypes::TITLE] = "title"; roles[UserDataTypes::DESCRIPTION] = "description"; - roles[UserDataTypes::SELECTED] = "selected"; roles[UserDataTypes::INSTALLED] = "installed"; return roles; @@ -142,9 +141,9 @@ void ResourceModel::search() if (m_search_term.startsWith("#")) { auto projectId = m_search_term.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { + callbacks.on_fail = [this](QString reason, int) { if (!s_running_models.constFind(this).value()) return; searchRequestFailed(reason, -1); @@ -155,45 +154,43 @@ void ResourceModel::search() searchRequestAborted(); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { + callbacks.on_succeed = [this](auto& pack) { if (!s_running_models.constFind(this).value()) return; - searchRequestForOneSucceeded(doc); + searchRequestForOneSucceeded(pack); }; - if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) + auto project = std::make_shared(); + project->addonId = projectId; + if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job) runSearchJob(job); return; } } auto args{ createSearchArguments() }; - auto callbacks{ createSearchCallbacks() }; + ResourceAPI::Callback> callbacks{}; - // Use defaults if no callbacks are set - if (!callbacks.on_succeed) - callbacks.on_succeed = [this](auto& doc) { - if (!s_running_models.constFind(this).value()) - return; - searchRequestSucceeded(doc); - }; - if (!callbacks.on_fail) - callbacks.on_fail = [this](QString reason, int network_error_code) { - if (!s_running_models.constFind(this).value()) - return; - searchRequestFailed(reason, network_error_code); - }; - if (!callbacks.on_abort) - callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) - return; - searchRequestAborted(); - }; + callbacks.on_succeed = [this](auto& doc) { + 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()) + return; + searchRequestFailed(reason, network_error_code); + }; + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) runSearchJob(job); } -void ResourceModel::loadEntry(QModelIndex& entry) +void ResourceModel::loadEntry(const QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; @@ -202,14 +199,15 @@ void ResourceModel::loadEntry(QModelIndex& entry) if (!pack->versionsLoaded) { auto args{ createVersionsArguments(entry) }; - auto callbacks{ createVersionsCallbacks(entry) }; + ResourceAPI::Callback> callbacks{}; + auto addonId = pack->addonId; // Use default if no callbacks are set if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + callbacks.on_succeed = [this, entry, addonId](auto& doc) { if (!s_running_models.constFind(this).value()) return; - versionRequestSucceeded(doc, pack, entry); + versionRequestSucceeded(doc, addonId, entry); }; if (!callbacks.on_fail) callbacks.on_fail = [](QString reason, int) { @@ -223,28 +221,23 @@ void ResourceModel::loadEntry(QModelIndex& entry) if (!pack->extraDataLoaded) { auto args{ createInfoArguments(entry) }; - auto callbacks{ createInfoCallbacks(entry) }; + ResourceAPI::Callback callbacks{}; - // Use default if no callbacks are set - if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) { - if (!s_running_models.constFind(this).value()) - return; - auto pack = newpack; - infoRequestSucceeded(doc, pack, entry); - }; - if (!callbacks.on_fail) - callbacks.on_fail = [this](QString reason) { - if (!s_running_models.constFind(this).value()) - return; - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); - }; - if (!callbacks.on_abort) - callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) - return; - qCritical() << tr("The request was aborted for an unknown reason"); - }; + callbacks.on_succeed = [this, entry](auto& newpack) { + 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()) + 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()) + return; + qCritical() << tr("The request was aborted for an unknown reason"); + }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); @@ -337,7 +330,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry); auto full_file_path = cache_entry->getFullPath(); - connect(icon_fetch_action.get(), &Task::succeeded, this, [=] { + connect(icon_fetch_action.get(), &Task::succeeded, this, [this, url, full_file_path, index] { auto icon = QIcon(full_file_path); QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); @@ -345,7 +338,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) emit dataChanged(index, index, { Qt::DecorationRole }); }); - connect(icon_fetch_action.get(), &Task::failed, this, [=] { + connect(icon_fetch_action.get(), &Task::failed, this, [this, url] { m_currently_running_icon_actions.remove(url); m_failed_icon_actions.insert(url); }); @@ -359,68 +352,35 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return {}; } -// No 'forgor to implement' shall pass here :blobfox_knife: -#define NEED_FOR_CALLBACK_ASSERT(name) \ - Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.") - -QJsonArray ResourceModel::documentToArray([[maybe_unused]] QJsonDocument& doc) const -{ - NEED_FOR_CALLBACK_ASSERT("documentToArray"); - return {}; -} -void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) -{ - NEED_FOR_CALLBACK_ASSERT("loadIndexedPack"); -} -void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) -{ - NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo"); -} -void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) -{ - NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions"); -} - /* Default callbacks */ -void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) +void ResourceModel::searchRequestSucceeded(QList& newList) { - QList newList; - auto packs = documentToArray(doc); - - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack::Ptr pack = std::make_shared(); - try { - loadIndexedPack(*pack, packObj); - 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; - }); - sel != m_selected.end()) { - newList.append(sel->get()->getPack()); - } else - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); - continue; + 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; + }); + sel != m_selected.end()) { + p = sel->get()->getPack(); + } else { + p = pack; + } + if (checkFilters(p)) { + filteredNewList << p; } } - if (packs.size() < 25) { + if (newList.size() < 25) { m_search_state = SearchState::Finished; } else { m_next_search_offset += 25; m_search_state = SearchState::CanFetchMore; } - QList filteredNewList; - for (auto p : newList) - if (checkFilters(p)) - filteredNewList << p; - // When you have a Qt build with assertions turned on, proceeding here will abort the application if (filteredNewList.size() == 0) return; @@ -430,20 +390,8 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) endInsertRows(); } -void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { - ModPlatform::IndexedPack::Ptr pack = std::make_shared(); - - try { - auto obj = Json::requireObject(doc); - if (obj.contains("data")) - obj = Json::requireObject(obj, "data"); - loadIndexedPack(*pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); - } - m_search_state = SearchState::Finished; beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); @@ -480,21 +428,16 @@ void ResourceModel::searchRequestAborted() search(); } -void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +void ResourceModel::versionRequestSucceeded(QVector& doc, QVariant pack, const QModelIndex& index) { auto current_pack = 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 != current_pack->addonId) return; - try { - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - loadIndexedPackVersions(*current_pack, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); - } + current_pack->versions = doc; + current_pack->versionsLoaded = true; // Cache info :^) QVariant new_pack; @@ -504,44 +447,35 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind return; } - emit versionListUpdated(); + emit versionListUpdated(index); } -void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, const QModelIndex& index) { auto current_pack = 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 != current_pack->addonId) return; - try { - auto obj = Json::requireObject(doc); - loadExtraPackInfo(*current_pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); - } - // Cache info :^) QVariant new_pack; - new_pack.setValue(current_pack); + new_pack.setValue(pack); if (!setData(index, new_pack, Qt::UserRole)) { qWarning() << "Failed to cache resource info!"; return; } - emit projectInfoUpdated(); + emit projectInfoUpdated(index); } void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - const std::shared_ptr packs, - bool is_indexed, - QString custom_target_folder) + ResourceFolderModel* packs, + bool is_indexed) { version.is_currently_selected = true; - m_selected.append(makeShared(pack, version, packs, is_indexed, custom_target_folder)); + m_selected.append(makeShared(pack, version, packs, is_indexed)); } void ResourceModel::removePack(const QString& rem) diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 4c7ea33a0..573ad8b75 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -36,25 +36,22 @@ class ResourceModel : public QAbstractListModel { ResourceModel(ResourceAPI* api); ~ResourceModel() override; - [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; - [[nodiscard]] auto roleNames() const -> QHash override; + auto data(const QModelIndex&, int role) const -> QVariant override; + auto roleNames() const -> QHash override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - [[nodiscard]] virtual auto debugName() const -> QString; - [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString; + virtual auto metaEntryBase() const -> QString = 0; - [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override - { - return parent.isValid() ? 0 : static_cast(m_packs.size()); - } - [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } - [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } + 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); } - [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } - [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } + bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } + bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } - [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } + auto getSortingMethods() const { return m_api->getSortingMethods(); } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const { return {}; } /** Whether the version is opted out or not. Currently only makes sense in CF. */ @@ -69,7 +66,6 @@ class ResourceModel : public QAbstractListModel { public slots: void fetchMore(const QModelIndex& parent) override; - // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 inline bool canFetchMore(const QModelIndex& parent) const override { return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; @@ -78,19 +74,16 @@ class ResourceModel : public QAbstractListModel { void setSearchTerm(QString term) { m_search_term = term; } virtual ResourceAPI::SearchArgs createSearchArguments() = 0; - virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } - virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; - virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0; /** Requests the API for more entries. */ virtual void search(); /** Applies any processing / extra requests needed to fully load the specified entry's information. */ - virtual void loadEntry(QModelIndex&); + virtual void loadEntry(const QModelIndex&); /** Schedule a refresh, clearing the current state. */ void refresh(); @@ -100,9 +93,8 @@ class ResourceModel : public QAbstractListModel { void addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - std::shared_ptr packs, - bool is_indexed = false, - QString custom_target_folder = {}); + ResourceFolderModel* packs, + bool is_indexed = false); void removePack(const QString& rem); QList selectedPacks() { return m_selected; } @@ -113,23 +105,7 @@ class ResourceModel : public QAbstractListModel { void runSearchJob(Task::Ptr); void runInfoJob(Task::Ptr); - [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional; - - /** Converts a JSON document to a common array format. - * - * This is needed so that different providers, with different JSON structures, can be parsed - * uniformally. You NEED to re-implement this if you intend on using the default callbacks. - */ - [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; - - /** Functions to load data into a pack. - * - * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. - */ - - virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&); - virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&); - virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); + auto getCurrentSortingMethodByIndex() const -> std::optional; virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; } @@ -160,18 +136,18 @@ class ResourceModel : public QAbstractListModel { private: /* Default search request callbacks */ - void searchRequestSucceeded(QJsonDocument&); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestSucceeded(QList&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); - void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + void versionRequestSucceeded(QVector&, QVariant, const QModelIndex&); - void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + void infoRequestSucceeded(ModPlatform::IndexedPack::Ptr, const QModelIndex&); signals: - void versionListUpdated(); - void projectInfoUpdated(); + void versionListUpdated(const QModelIndex& index); + void projectInfoUpdated(const QModelIndex& index); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index d436f320f..53a40faa2 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -8,8 +8,11 @@ namespace ResourceDownload { -ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst) +ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, + ResourceAPI* api, + QString debugName, + QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ @@ -17,19 +20,19 @@ ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_in ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort }; + return { ModPlatform::ResourceType::ResourcePack, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { - auto& pack = m_packs[entry.row()]; - return { *pack }; + auto pack = m_packs[entry.row()]; + return { pack, {}, {}, ModPlatform::ResourceType::ResourcePack }; } -ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) { - auto& pack = m_packs[entry.row()]; - return { *pack }; + auto pack = m_packs[entry.row()]; + return { 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 e2b4a1957..d664ccb05 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.h +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -20,24 +20,25 @@ class ResourcePackResourceModel : public ResourceModel { Q_OBJECT public: - ResourcePackResourceModel(BaseInstance const&, ResourceAPI*); + ResourcePackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); - void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index 99039476e..8a7ed2720 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -14,9 +14,7 @@ namespace ResourceDownload { ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected); -} +{} /******** Callbacks to events in the UI (set up in the derived classes) ********/ diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h index 440d91ab0..f8d4d5bf9 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -25,22 +25,23 @@ class ResourcePackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'resource pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("resource packs"); } + inline QString resourcesString() const override { return tr("resource packs"); } //: The singular version of 'resource packs' - [[nodiscard]] inline QString resourceString() const override { return tr("resource pack"); } + inline QString resourceString() const override { return tr("resource pack"); } - [[nodiscard]] bool supportsFiltering() const override { return false; }; + bool supportsFiltering() const override { return false; }; - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; - [[nodiscard]] inline auto helpPage() const -> QString override { return "resourcepack-platform"; } + inline auto helpPage() const -> QString override { return "resourcepack-platform"; } protected: ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 766070f20..fc7385937 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -42,12 +42,13 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_ResourcePage.h" +#include #include #include #include "Markdown.h" -#include "StringUtils.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" @@ -77,10 +78,15 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); - m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + auto delegate = new ProjectItemDelegate(this); + m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); + m_ui->packView->viewport()->installEventFilter(this); connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); + + connect(m_ui->packView, &QAbstractItemView::doubleClicked, this, &ResourcePage::onResourceToggle); + connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onResourceToggle); } ResourcePage::~ResourcePage() @@ -127,17 +133,20 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool m_searchTimer.start(350); } } else if (watched == m_ui->packView) { + // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { - onResourceSelected(); - - // To have the 'select mod' button outlined instead of the 'review and confirm' one - m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); - m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason); - + onResourceToggle(m_ui->packView->currentIndex()); keyEvent->accept(); return true; } } + } else if (watched == m_ui->packView->viewport() && event->type() == QEvent::MouseButtonPress) { + auto* mouseEvent = static_cast(event); + + if (mouseEvent->button() == Qt::MiddleButton) { + onResourceToggle(m_ui->packView->indexAt(mouseEvent->pos())); + return true; + } } return QWidget::eventFilter(watched, event); @@ -176,8 +185,11 @@ ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } -void ResourcePage::updateUi() +void ResourcePage::updateUi(const QModelIndex& index) { + if (index != m_ui->packView->currentIndex()) + return; + auto current_pack = getCurrentPack(); if (!current_pack) { m_ui->packDescription->setHtml({}); @@ -267,35 +279,48 @@ void ResourcePage::updateSelectionButton() } } -void ResourcePage::updateVersionList() +void ResourcePage::versionListUpdated(const QModelIndex& index) { - auto current_pack = getCurrentPack(); + if (index == m_ui->packView->currentIndex()) { + auto current_pack = getCurrentPack(); - m_ui->versionSelectionBox->blockSignals(true); - m_ui->versionSelectionBox->clear(); - m_ui->versionSelectionBox->blockSignals(false); + m_ui->versionSelectionBox->blockSignals(true); + m_ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->blockSignals(false); - if (current_pack) { - auto installedVersion = m_model->getInstalledPackVersion(current_pack); + if (current_pack) { + auto installedVersion = m_model->getInstalledPackVersion(current_pack); - for (int i = 0; i < current_pack->versions.size(); i++) { - auto& version = current_pack->versions[i]; - if (!m_model->checkVersionFilters(version)) - continue; + for (int i = 0; i < current_pack->versions.size(); i++) { + auto& version = current_pack->versions[i]; + if (!m_model->checkVersionFilters(version)) + continue; - auto release_type = current_pack->versions[i].version_type.isValid() - ? QString(" [%1]").arg(current_pack->versions[i].version_type.toString()) - : ""; + auto versionText = version.version; + if (version.version_type.isValid()) { + versionText += QString(" [%1]").arg(version.version_type.toString()); + } + if (version.fileId == installedVersion) { + versionText += tr(" [installed]", "Mod version select"); + } - m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i)); + m_ui->versionSelectionBox->addItem(versionText, QVariant(i)); + } + } + if (m_ui->versionSelectionBox->count() == 0) { + m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); + m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); } - } - if (m_ui->versionSelectionBox->count() == 0) { - m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); - m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); - } - updateSelectionButton(); + if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onResourceToggle(index); + } else + updateSelectionButton(); + } else if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onResourceToggle(index); + } } void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) @@ -313,16 +338,20 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI request_load = true; } else { - updateVersionList(); + versionListUpdated(curr); } if (current_pack && !current_pack->extraDataLoaded) request_load = true; + // we are already requesting this + if (m_enableQueue.contains(curr.row())) + request_load = false; + if (request_load) m_model->loadEntry(curr); - updateUi(); + updateUi(curr); } void ResourcePage::onVersionSelectionChanged(int index) @@ -341,11 +370,15 @@ void ResourcePage::removeResourceFromDialog(const QString& pack_name) m_parentDialog->removeResource(pack_name); } -void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, - ModPlatform::IndexedVersion& ver, - const std::shared_ptr base_model) +void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver, ResourceFolderModel* base_model) { - m_model->addPack(pack, ver, base_model); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(pack, ver, base_model, is_indexed); +} + +void ResourcePage::modelReset() +{ + m_enableQueue.clear(); } void ResourcePage::removeResourceFromPage(const QString& name) @@ -379,6 +412,48 @@ void ResourcePage::onResourceSelected() m_ui->packView->repaint(); } +void ResourcePage::onResourceToggle(const QModelIndex& index) +{ + const bool isSelected = index == m_ui->packView->currentIndex(); + auto pack = m_model->data(index, Qt::UserRole).value(); + + if (pack->versionsLoaded) { + if (pack->isAnyVersionSelected()) + removeResourceFromDialog(pack->name); + else { + auto version = std::find_if(pack->versions.begin(), pack->versions.end(), [this](const ModPlatform::IndexedVersion& version) { + return m_model->checkVersionFilters(version); + }); + + if (version == pack->versions.end()) { + auto errorMessage = new QMessageBox( + QMessageBox::Warning, tr("No versions available"), + tr("No versions for '%1' are available.\nThe author likely blocked third-party launchers.").arg(pack->name), + QMessageBox::Ok, this); + + errorMessage->open(); + } else + addResourceToDialog(pack, *version); + } + + if (isSelected) + updateSelectionButton(); + + // force update + QVariant variant; + variant.setValue(pack); + m_model->setData(index, variant, Qt::UserRole); + } else { + // the model is just 1 dimensional so this is fine + m_enableQueue.insert(index.row()); + + // we can't be sure that this hasn't already been requested... + // but this does the job well enough and there's not much point preventing edgecases + if (!isSelected) + m_model->loadEntry(index); + } +} + void ResourcePage::openUrl(const QUrl& url) { // do not allow other url schemes for security reasons @@ -477,7 +552,7 @@ void ResourcePage::openProject(QVariant projectID) connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); auto jump = [this] { diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 09c512df4..6e219bf22 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -33,37 +33,37 @@ class ResourcePage : public QWidget, public BasePage { ~ResourcePage() override; /* Affects what the user sees */ - [[nodiscard]] auto displayName() const -> QString override = 0; - [[nodiscard]] auto icon() const -> QIcon override = 0; - [[nodiscard]] auto id() const -> QString override = 0; - [[nodiscard]] auto helpPage() const -> QString override = 0; - [[nodiscard]] bool shouldDisplay() const override = 0; + auto displayName() const -> QString override = 0; + auto icon() const -> QIcon override = 0; + auto id() const -> QString override = 0; + auto helpPage() const -> QString override = 0; + bool shouldDisplay() const override = 0; /* Used internally */ - [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; - [[nodiscard]] virtual auto debugName() const -> QString = 0; + virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString = 0; //: The plural version of 'resource' - [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); } + virtual inline QString resourcesString() const { return tr("resources"); } //: The singular version of 'resources' - [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); } + virtual inline QString resourceString() const { return tr("resource"); } /* Features this resource's page supports */ - [[nodiscard]] virtual bool supportsFiltering() const = 0; + virtual bool supportsFiltering() const = 0; void retranslate() override; void openedImpl() override; auto eventFilter(QObject* watched, QEvent* event) -> bool override; /** Get the current term in the search bar. */ - [[nodiscard]] auto getSearchTerm() const -> QString; + auto getSearchTerm() const -> QString; /** Programatically set the term in the search bar. */ void setSearchTerm(QString); - [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr); - [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; - [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } - [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } + bool setCurrentPack(ModPlatform::IndexedPack::Ptr); + auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; + auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } + auto getModel() const -> ResourceModel* { return m_model; } protected: ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); @@ -71,14 +71,16 @@ class ResourcePage : public QWidget, public BasePage { void addSortings(); public slots: - virtual void updateUi(); + virtual void updateUi(const QModelIndex& index); virtual void updateSelectionButton(); - virtual void updateVersionList(); + virtual void versionListUpdated(const QModelIndex& index); void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResourceFromDialog(const QString& pack_name); virtual void removeResourceFromPage(const QString& name); - virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr); + virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, ResourceFolderModel*); + + virtual void modelReset(); QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } @@ -91,8 +93,7 @@ class ResourcePage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void onResourceSelected(); - - // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 + void onResourceToggle(const QModelIndex& index); /** Associates regex expressions to pages in the order they're given in the map. */ virtual QMap urlHandlers() const = 0; @@ -115,6 +116,8 @@ class ResourcePage : public QWidget, public BasePage { QTimer m_searchTimer; bool m_doNotJumpToMod = false; + + QSet m_enableQueue; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index 8c913657a..5349b69ab 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -8,8 +8,8 @@ namespace ResourceDownload { -ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst) +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) {} /******** Make data requests ********/ @@ -17,19 +17,19 @@ ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort }; + return { ModPlatform::ResourceType::ShaderPack, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { - auto& pack = m_packs[entry.row()]; - return { *pack }; + auto pack = m_packs[entry.row()]; + return { pack, {}, {}, ModPlatform::ResourceType::ShaderPack }; } -ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) { - auto& pack = m_packs[entry.row()]; - return { *pack }; + auto pack = m_packs[entry.row()]; + return { 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 f3c695e9f..9856be93e 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.h +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -20,24 +20,25 @@ class ShaderPackResourceModel : public ResourceModel { Q_OBJECT public: - ShaderPackResourceModel(BaseInstance const&, ResourceAPI*); + ShaderPackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); - void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index cedf985fb..99c50352a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -8,16 +8,14 @@ #include "ShaderPackModel.h" +#include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include namespace ResourceDownload { -ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected); -} +ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ @@ -46,12 +44,10 @@ QMap ShaderPackResourcePage::urlHandlers() const void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, - const std::shared_ptr base_model) + ResourceFolderModel* base_model) { - QString custom_target_folder; - if (version.loaders & ModPlatform::Cauldron) - custom_target_folder = QStringLiteral("resourcepacks"); - m_model->addPack(pack, version, base_model, false, custom_target_folder); + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_model->addPack(pack, version, base_model, is_indexed); } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index 4b92c33dc..92ddd9f8a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -25,24 +25,25 @@ class ShaderPackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'shader pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("shader packs"); } + inline QString resourcesString() const override { return tr("shader packs"); } //: The singular version of 'shader packs' - [[nodiscard]] inline QString resourceString() const override { return tr("shader pack"); } + inline QString resourceString() const override { return tr("shader pack"); } - [[nodiscard]] bool supportsFiltering() const override { return false; }; + bool supportsFiltering() const override { return false; }; - void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; + void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, ResourceFolderModel*) override; - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; - [[nodiscard]] inline auto helpPage() const -> QString override { return "shaderpack-platform"; } + inline auto helpPage() const -> QString override { return "shaderpack-platform"; } protected: ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index cb4cafd41..7c1490671 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -12,8 +12,8 @@ static std::list s_availableVersions = {}; namespace ResourceDownload { -TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api) - : ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) +TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourcePackResourceModel(inst, api, debugName, metaEntryBase), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; @@ -70,9 +70,10 @@ ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto args = ResourcePackResourceModel::createVersionsArguments(entry); + args.resourceType = ModPlatform::ResourceType::TexturePack; if (!m_version_list->isLoaded()) { qCritical() << "The version list could not be loaded. Falling back to showing all entries."; return args; diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index 607a03be3..bb7348b33 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -13,12 +13,12 @@ class TexturePackResourceModel : public ResourcePackResourceModel { Q_OBJECT public: - TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api); + TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase); - [[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } + inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; protected: Meta::VersionList::Ptr m_version_list; diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 42aa921c5..262004dfd 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -27,22 +27,20 @@ class TexturePackResourcePage : public ResourcePackResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'texture pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("texture packs"); } + inline QString resourcesString() const override { return tr("texture packs"); } //: The singular version of 'texture packs' - [[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); } + inline QString resourceString() const override { return tr("texture pack"); } protected: - TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) - { - connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected); - } + TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) {} }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index dee3784e5..6868ce736 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -68,7 +68,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); + if (searchTerm.startsWith("#")) return QString::number(pack.id) == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); @@ -76,8 +79,12 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (currentSorting == ByPopularity) { return leftPack.position > rightPack.position; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index f116ca915..ed25c9fdf 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -61,7 +61,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.safeName)) { return (m_logoMap.value(pack.safeName)); } - auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + auto icon = QIcon::fromTheme("atlauncher-placeholder"); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(pack.safeName); ((ListModel*)this)->requestLogo(pack.safeName, url); @@ -82,8 +82,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -101,12 +99,12 @@ 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)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response.get())); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); + connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::requestFinished() @@ -199,7 +197,7 @@ void ListModel::requestLogo(QString file, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { @@ -207,7 +205,7 @@ void ListModel::requestLogo(QString file, QString url) } }); - QObject::connect(job, &NetJob::failed, this, [this, file, job] { + connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h index bcadd7c91..7311d787a 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -61,7 +61,7 @@ class ListModel : public QAbstractListModel { QMap waitingCallbacks; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + std::unique_ptr response = std::make_unique(); }; } // namespace Atl diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 6fb867733..7d24726b5 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -45,7 +45,9 @@ #include "net/ApiDownload.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods) +AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, + const ATLauncher::PackVersion& version, + QList mods) : QAbstractListModel(parent), m_version(version), m_mods(mods) { // fill mod index @@ -62,9 +64,9 @@ AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::Pa } } -QVector AtlOptionalModListModel::getResult() +QList AtlOptionalModListModel::getResult() { - QVector result; + QList result; for (const auto& mod : m_mods) { if (m_selection[mod.name]) { @@ -157,7 +159,7 @@ 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)); + m_jobPtr->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), m_response.get())); connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); connect(m_jobPtr.get(), &NetJob::failed, this, &AtlOptionalModListModel::shareCodeFailure); @@ -233,7 +235,7 @@ void AtlOptionalModListModel::clearAll() emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } -void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) +void AtlOptionalModListModel::toggleMod(const ATLauncher::VersionMod& mod, int index) { auto enable = !m_selection[mod.name]; @@ -251,7 +253,7 @@ void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) setMod(mod, index, enable); } -void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) +void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit) { if (m_selection[mod.name] == enable) return; @@ -313,7 +315,7 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } } -AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods) +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods) : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { ui->setupUi(this); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 767d277d9..ebd63881b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -55,9 +55,9 @@ class AtlOptionalModListModel : public QAbstractListModel { DescriptionColumn, }; - AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods); + AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); - QVector getResult(); + QList getResult(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; @@ -78,29 +78,29 @@ class AtlOptionalModListModel : public QAbstractListModel { void clearAll(); private: - void toggleMod(ATLauncher::VersionMod mod, int index); - void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + void toggleMod(const ATLauncher::VersionMod& mod, int index); + void setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit = true); private: NetJob::Ptr m_jobPtr; - std::shared_ptr m_response = std::make_shared(); + std::unique_ptr m_response = std::make_unique(); ATLauncher::PackVersion m_version; - QVector m_mods; + QList m_mods; QMap m_selection; QMap m_index; - QMap> m_dependents; + QMap> m_dependents; }; class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods); + AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); ~AtlOptionalModDialog() override; - QVector getResult() { return listModel->getResult(); } + QList getResult() { return listModel->getResult(); } void useShareCode(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 7c69b315e..ad49d940e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -143,7 +143,9 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex return; } - selected = filterModel->data(first, Qt::UserRole).value(); + QVariant raw = filterModel->data(first, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + selected = raw.value(); ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
    "))); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 73460232b..8c8bf53b3 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -41,7 +41,6 @@ #include #include -#include "Application.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" namespace Ui { @@ -57,7 +56,7 @@ class AtlPage : public QWidget, public ModpackProviderBasePage { explicit AtlPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~AtlPage(); virtual QString displayName() const override { return "ATLauncher"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("atlauncher"); } + virtual QIcon icon() const override { return QIcon::fromTheme("atlauncher"); } virtual QString id() const override { return "atl"; } virtual QString helpPage() const override { return "ATL-platform"; } virtual bool shouldDisplay() const override; @@ -68,7 +67,7 @@ class AtlPage : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp index 0c7257859..dc9a4758f 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp @@ -41,8 +41,8 @@ AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {} -std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(ATLauncher::PackVersion version, - QVector mods) +std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version, + QList mods) { AtlOptionalModDialog optionalModDialog(m_parent, version, mods); auto result = optionalModDialog.exec(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h index 52ced2615..99f907a19 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h @@ -48,7 +48,7 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInt private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; - std::optional> chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + std::optional> chooseOptionalMods(const ATLauncher::PackVersion& version, QList mods) override; void displayMessage(QString message) override; private: diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index cfdb185ff..5d968d65a 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace Flame { @@ -20,7 +21,7 @@ ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : modpacks.size(); + return parent.isValid() ? 0 : m_modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const @@ -31,27 +32,27 @@ int ListModel::columnCount(const QModelIndex& parent) const QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } - IndexedPack pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { - if (pack.description.length() > 100) { + if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); + QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } - return pack.description; + return pack->description; } case Qt::DecorationRole: { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + if (m_logoMap.contains(pack->logoName)) { + return (m_logoMap.value(pack->logoName)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); + ((ListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { @@ -62,11 +63,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const case Qt::SizeHintRole: return QSize(0, 58); case UserDataTypes::TITLE: - return pack.name; + return pack->name; case UserDataTypes::DESCRIPTION: - return pack.description; - case UserDataTypes::SELECTED: - return false; + return pack->description; case UserDataTypes::INSTALLED: return false; default: @@ -78,10 +77,10 @@ QVariant ListModel::data(const QModelIndex& index, int role) const bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; - modpacks[pos] = value.value(); + m_modpacks[pos] = value.value(); return true; } @@ -90,8 +89,8 @@ void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); - for (int i = 0; i < modpacks.size(); i++) { - if (modpacks[i].logoName == logo) { + for (int i = 0; i < m_modpacks.size(); i++) { + if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } @@ -115,15 +114,15 @@ void ListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); + if (m_waitingCallbacks.contains(logo)) { + m_waitingCallbacks.value(logo)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); @@ -149,14 +148,14 @@ Qt::ItemFlags ListModel::flags(const QModelIndex& index) const bool ListModel::canFetchMore([[maybe_unused]] const QModelIndex& parent) const { - return searchState == CanPossiblyFetchMore; + return m_searchState == CanPossiblyFetchMore; } void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if (nextSearchOffset == 0) { + if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -165,137 +164,112 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - if (currentSearchTerm.startsWith("#")) { - auto projectId = currentSearchTerm.mid(1); + static const FlameAPI api; + if (m_currentSearchTerm.startsWith("#")) { + auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; - static const FlameAPI api; - if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { - jobPtr = job; - jobPtr->start(); + auto project = std::make_shared(); + project->addonId = projectId; + if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { + m_jobPtr = job; + m_jobPtr->start(); } return; } } ResourceAPI::SortingMethod sort{}; - sort.index = currentSort + 1; + sort.index = m_currentSort + 1; + + ResourceAPI::Callback> callbacks{}; + + callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_abort = [this] { + qCritical() << "Search task aborted by an unknown reason!"; + searchRequestFailed("Aborted"); + }; - auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, - m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); + auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, + std::move(callbacks)); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); - jobPtr = netJob; - jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); + m_jobPtr = netJob; + m_jobPtr->start(); } void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) { + if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort && !filterChanged) { return; } - currentSearchTerm = term; - currentSort = sort; + m_currentSearchTerm = term; + m_currentSort = sort; m_filter = filter; if (hasActiveSearchJob()) { - jobPtr->abort(); - searchState = ResetRequested; + m_jobPtr->abort(); + m_searchState = ResetRequested; return; } beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - searchState = None; + m_searchState = None; - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } -void Flame::ListModel::searchRequestFinished() +void Flame::ListModel::searchRequestFinished(QList& newList) { if (hasActiveSearchJob()) return; - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - QList newList; - auto packs = Json::ensureArray(doc.object(), "data"); - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - Flame::IndexedPack pack; - try { - Flame::loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading pack from CurseForge: " << e.cause(); - continue; - } - } - if (packs.size() < 25) { - searchState = Finished; + if (newList.size() < 25) { + m_searchState = Finished; } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; + m_nextSearchOffset += 25; + m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); + m_modpacks.append(newList); endInsertRows(); } -void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { - jobPtr.reset(); - - auto packObj = Json::ensureObject(doc.object(), "data"); - - Flame::IndexedPack pack; - try { - Flame::loadIndexedPack(pack, packObj); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading pack from CurseForge: " << e.cause(); - return; - } + m_jobPtr.reset(); - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); - modpacks.append({ pack }); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); + m_modpacks.append(pack); endInsertRows(); } void Flame::ListModel::searchRequestFailed(QString reason) { - jobPtr.reset(); + m_jobPtr.reset(); - if (searchState == ResetRequested) { + if (m_searchState == ResetRequested) { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } else { - searchState = Finished; + m_searchState = Finished; } } diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 026f6d1ee..92ff098d4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -16,8 +16,6 @@ #include #include "ui/widgets/ModFilterWidget.h" -#include - namespace Flame { using LogoMap = QMap; @@ -41,8 +39,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } private slots: void performPaginatedSearch(); @@ -50,27 +48,26 @@ class ListModel : public QAbstractListModel { void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); - void searchRequestFinished(); + void searchRequestFinished(QList&); void searchRequestFailed(QString reason); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); private: void requestLogo(QString file, QString url); private: - QList modpacks; + QList m_modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; - QMap waitingCallbacks; + QMap m_waitingCallbacks; - QString currentSearchTerm; - int currentSort = 0; + QString m_currentSearchTerm; + int m_currentSort = 0; std::shared_ptr m_filter; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - Task::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + int m_nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; + Task::Ptr m_jobPtr; }; } // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index de6b3d633..316295390 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -35,7 +35,8 @@ #include "FlamePage.h" #include "Version.h" -#include "modplatform/flame/FlamePackIndex.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" @@ -43,29 +44,25 @@ #include #include -#include "Application.h" #include "FlameModel.h" #include "InstanceImportTask.h" -#include "Json.h" #include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" -#include "net/ApiDownload.h" - static FlameAPI api; FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) - : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false) + : QWidget(parent), m_ui(new Ui::FlamePage), m_dialog(dialog), m_fetch_progress(this, false) { - ui->setupUi(this); - ui->searchEdit->installEventFilter(this); - listModel = new Flame::ListModel(this); - ui->packView->setModel(listModel); + m_ui->setupUi(this); + m_ui->searchEdit->installEventFilter(this); + m_listModel = new Flame::ListModel(this); + m_ui->packView->setModel(m_listModel); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); @@ -76,33 +73,33 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->verticalLayout->insertWidget(2, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(2, &m_fetch_progress); // index is used to set the sorting with the curseforge api - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); - ui->sortByBox->addItem(tr("Sort by Total Downloads")); - - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); - connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlamePage::onVersionSelectionChanged); - - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packDescription->setMetaEntry("FlamePacks"); + m_ui->sortByBox->addItem(tr("Sort by Featured")); + m_ui->sortByBox->addItem(tr("Sort by Popularity")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); + m_ui->sortByBox->addItem(tr("Sort by Name")); + m_ui->sortByBox->addItem(tr("Sort by Author")); + m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); + + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged); + + m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packDescription->setMetaEntry("FlamePacks"); createFilterWidget(); } FlamePage::~FlamePage() { - delete ui; + delete m_ui; } bool FlamePage::eventFilter(QObject* watched, QEvent* event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); @@ -125,7 +122,7 @@ bool FlamePage::shouldDisplay() const void FlamePage::retranslate() { - ui->retranslateUi(this); + m_ui->retranslateUi(this); } void FlamePage::openedImpl() @@ -136,112 +133,91 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { - ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), - m_filterWidget->changed()); - m_fetch_progress.watch(listModel->activeSearchJob().get()); -} - -bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr filter) -{ - if (!filter) - return true; - return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders - (filter->releases.empty() || // releases - std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && - filter->checkMcVersions({ v.mcVersion })); // mcVersions} + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + bool filterChanged = m_filterWidget->changed(); + m_listModel->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); + m_fetch_progress.watch(m_listModel->activeSearchJob().get()); } void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { - ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); } return; } - current = listModel->data(curr, Qt::UserRole).value(); + m_current = m_listModel->data(curr, Qt::UserRole).value(); - if (!current.versionsLoaded || m_filterWidget->changed()) { + if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; - auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - int addonId = current.addonId; - netJob->addNetAction( - Net::ApiDownload::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { - if (addonId != current.addonId) { + + ResourceAPI::Callback > callbacks{}; + + auto addonId = m_current->addonId; + // Use default if no callbacks are set + callbacks.on_succeed = [this, curr, addonId](auto& doc) { + if (addonId != m_current->addonId) { return; // wrong request } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - auto arr = Json::ensureArray(doc.object(), "data"); - try { - Flame::loadIndexedPackVersions(current, arr); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading flame modpack version: " << e.cause(); - } - auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; + m_current->versions = doc; + m_current->versionsLoaded = true; + auto pred = [this](const ModPlatform::IndexedVersion& v) { + if (auto filter = m_filterWidget->getFilter()) + return !filter->checkModpackFilters(v); + return false; + }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) - current.versions.removeIf(pred); + m_current->versions.removeIf(pred); #else - for (auto it = current.versions.begin(); it != current.versions.end();) - if (pred(*it)) - it = current.versions.erase(it); - else - ++it; + for (auto it = m_current->versions.begin(); it != m_current->versions.end();) + if (pred(*it)) + it = m_current->versions.erase(it); + else + ++it; #endif - for (auto version : current.versions) { - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) - ? QString(" for %1").arg(version.mcVersion) - : ""; - ui->versionSelectionBox->addItem(QString("%1%2%3").arg(version.version, mcVersion, release_type), - QVariant(version.downloadUrl)); + for (auto version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.downloadUrl)); } QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(m_current); - if (!listModel->setData(curr, current_updated, Qt::UserRole)) + if (!m_listModel->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. - if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { - ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { + m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } suggestCurrent(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + }; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + + auto netJob = api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + + m_job = netJob; netJob->start(); } else { - for (auto version : current.versions) { - ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + for (auto version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } suggestCurrent(); } // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. - if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { - ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { + m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } updateUi(); @@ -254,26 +230,26 @@ void FlamePage::suggestCurrent() } if (m_selected_version_index == -1) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); return; } - auto version = current.versions.at(m_selected_version_index); + auto version = m_current->versions.at(m_selected_version_index); QMap extra_info; - extra_info.insert("pack_id", QString::number(current.addonId)); - extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("pack_id", m_current->addonId.toString()); + extra_info.insert("pack_version_id", version.fileId.toString()); - dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); - QString editedLogoName = "curseforge_" + current.logoName; - listModel->getLogo(current.logoName, current.logoUrl, - [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); + m_dialog->setSuggestedPack(m_current->name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); + QString editedLogoName = "curseforge_" + m_current->logoName; + m_listModel->getLogo(m_current->logoName, m_current->logoUrl, + [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; - ui->versionSelectionBox->itemData(index).toInt(&is_blocked); + m_ui->versionSelectionBox->itemData(index).toInt(&is_blocked); if (index == -1 || is_blocked) { m_selected_version_index = -1; @@ -282,7 +258,7 @@ void FlamePage::onVersionSelectionChanged(int index) m_selected_version_index = index; - Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); + Q_ASSERT(m_current->versions.at(m_selected_version_index).downloadUrl == m_ui->versionSelectionBox->currentData().toString()); suggestCurrent(); } @@ -290,72 +266,73 @@ void FlamePage::onVersionSelectionChanged(int index) void FlamePage::updateUi() { QString text = ""; - QString name = current.name; + QString name = m_current->name; - if (current.extra.websiteUrl.isEmpty()) + if (m_current->websiteUrl.isEmpty()) text = name; else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor& author) { + text = "websiteUrl + "\">" + name + ""; + if (!m_current->authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; - for (auto& author : current.authors) { + for (auto& author : m_current->authors) { authorStrs.push_back(authorToStr(author)); } text += "
    " + tr(" by ") + authorStrs.join(", "); } - if (current.extraInfoLoaded) { - if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty()) { + if (m_current->extraDataLoaded) { + if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() || + !m_current->extraData.wikiUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } - if (!current.extra.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current.extra.issuesUrl) + "
    "; - if (!current.extra.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current.extra.wikiUrl) + "
    "; - if (!current.extra.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current.extra.sourceUrl) + "
    "; + if (!m_current->extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; + if (!m_current->extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; + if (!m_current->extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; } text += "
    "; - text += api.getModDescription(current.addonId).toUtf8(); + text += api.getModDescription(m_current->addonId.toInt()).toUtf8(); - ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); - ui->packDescription->flush(); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); + m_ui->packDescription->flush(); } QString FlamePage::getSerachTerm() const { - return ui->searchEdit->text(); + return m_ui->searchEdit->text(); } void FlamePage::setSearchTerm(QString term) { - ui->searchEdit->setText(term); + m_ui->searchEdit->setText(term); } void FlamePage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, false, this); + auto widget = ModFilterWidget::create(nullptr, false); m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } - connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); - m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = FlameAPI::loadModCategories(response); + m_categoriesTask = FlameAPI::getCategories(response.get(), ModPlatform::ResourceType::Modpack); + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response.get()); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 27c96d2f1..eb763229f 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -37,9 +37,8 @@ #include -#include -#include #include +#include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" @@ -61,7 +60,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { explicit FlamePage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~FlamePage(); virtual QString displayName() const override { return "CurseForge"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("flame"); } + virtual QIcon icon() const override { return QIcon::fromTheme("flame"); } virtual QString id() const override { return "flame"; } virtual QString helpPage() const override { return "Flame-platform"; } virtual bool shouldDisplay() const override; @@ -76,7 +75,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); @@ -88,10 +87,10 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { void createFilterWidget(); private: - Ui::FlamePage* ui = nullptr; - NewInstanceDialog* dialog = nullptr; - Flame::ListModel* listModel = nullptr; - Flame::IndexedPack current; + Ui::FlamePage* m_ui = nullptr; + NewInstanceDialog* m_dialog = nullptr; + Flame::ListModel* m_listModel = nullptr; + ModPlatform::IndexedPack::Ptr m_current; int m_selected_version_index = -1; @@ -100,6 +99,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; + Task::Ptr m_job; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index ae4562be4..a40e6d5a3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -8,7 +8,7 @@ #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlameModIndex.h" +#include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { @@ -17,97 +17,9 @@ static bool isOptedOut(const ModPlatform::IndexedVersion& ver) return ver.downloadUrl.isEmpty(); } -FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {} - -void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); -} - -auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion -{ - return FlameMod::loadDependencyVersions(m, arr, &m_base_instance); -} - -bool FlameModModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameResourcePackModel::FlameResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new FlameAPI) {} - -void FlameResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); -} - -bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {} - -void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); - - QVector filtered_versions(m.versions.size()); - - // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. - for (auto const& version : m.versions) { - auto const& mc_versions = version.mcVersion; - - if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), - [this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); })) - filtered_versions.push_back(version); - } - - m.versions = filtered_versions; -} +FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) + : TexturePackResourceModel(base, new FlameAPI, Flame::debugName(), Flame::metaEntryBase()) +{} ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() { @@ -122,7 +34,7 @@ ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(const QModelIndex& entry) { auto args = TexturePackResourceModel::createVersionsArguments(entry); @@ -137,37 +49,4 @@ bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) con return isOptedOut(ver); } -auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameShaderPackModel::FlameShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new FlameAPI) {} - -void FlameShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); -} - -bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 458fd85d0..76062f8e6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -5,52 +5,10 @@ #pragma once #include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/ResourcePackModel.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { -class FlameModModel : public ModModel { - Q_OBJECT - - public: - FlameModModel(BaseInstance&); - ~FlameModModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class FlameResourcePackModel : public ResourcePackResourceModel { - Q_OBJECT - - public: - FlameResourcePackModel(const BaseInstance&); - ~FlameResourcePackModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - class FlameTexturePackModel : public TexturePackResourceModel { Q_OBJECT @@ -61,36 +19,11 @@ class FlameTexturePackModel : public TexturePackResourceModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class FlameShaderPackModel : public ShaderPackResourceModel { - Q_OBJECT - - public: - FlameShaderPackModel(const BaseInstance&); - ~FlameShaderPackModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 4e01f3a65..af2de22ec 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -40,7 +40,6 @@ #include "FlameResourcePages.h" #include #include -#include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "ui_ResourcePage.h" @@ -51,16 +50,16 @@ namespace ResourceDownload { FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new FlameModModel(instance); + m_model = new ModModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -85,17 +84,16 @@ void FlameModPage::openUrl(const QUrl& url) FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { - m_model = new FlameResourcePackModel(instance); + m_model = new ResourcePackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -127,10 +125,9 @@ FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, Ba // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -152,25 +149,57 @@ void FlameTexturePackPage::openUrl(const QUrl& url) TexturePackResourcePage::openUrl(url); } +void FlameDataPackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + DataPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary + return; + } + } + + DataPackResourcePage::openUrl(url); +} + FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { - m_model = new FlameShaderPackModel(instance); + m_model = new ShaderPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } +FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) +{ + m_model = new DataPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + void FlameShaderPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -206,18 +235,22 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool { return true; } +auto FlameDataPackPage::shouldDisplay() const -> bool +{ + return true; +} -unique_qobject_ptr FlameModPage::createFilterWidget() +std::unique_ptr FlameModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), false, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), false); } void FlameModPage::prepareProviderCategories() { auto response = std::make_shared(); - m_categoriesTask = FlameAPI::getModCategories(response); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = FlameAPI::loadModCategories(response); + m_categoriesTask = FlameAPI::getModCategories(response.get()); + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response.get()); m_filter_widget->setCategories(categories); }); m_categoriesTask->start(); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 052706549..d4b697ae0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -39,7 +39,7 @@ #pragma once -#include "Application.h" +#include #include "modplatform/ResourceAPI.h" @@ -57,7 +57,7 @@ static inline QString displayName() } static inline QIcon icon() { - return APPLICATION->getThemedIcon("flame"); + return QIcon::fromTheme("flame"); } static inline QString id() { @@ -85,18 +85,18 @@ class FlameModPage : public ModPage { FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~FlameModPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; - unique_qobject_ptr createFilterWidget() override; + std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; @@ -117,15 +117,15 @@ class FlameResourcePackPage : public ResourcePackResourcePage { FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~FlameResourcePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; @@ -142,15 +142,15 @@ class FlameTexturePackPage : public TexturePackResourcePage { FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~FlameTexturePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; @@ -167,15 +167,40 @@ class FlameShaderPackPage : public ShaderPackResourcePage { FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~FlameShaderPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } + + void openUrl(const QUrl& url) override; +}; + +class FlameDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static FlameDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~FlameDataPackPage() override = default; + + bool shouldDisplay() const override; + + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 15303bb22..9a2b31768 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include "FileSystem.h" #include "ListModel.h" @@ -88,6 +89,34 @@ void ImportFTBPage::retranslate() ui->retranslateUi(this); } +QString saveIconToTempFile(const QIcon& icon) +{ + if (icon.isNull()) { + return QString(); + } + + QPixmap pixmap = icon.pixmap(icon.availableSizes().last()); + if (pixmap.isNull()) { + return QString(); + } + + QTemporaryFile tempFile(QDir::tempPath() + "/iconXXXXXX.png"); + tempFile.setAutoRemove(false); + if (!tempFile.open()) { + return QString(); + } + + QString tempPath = tempFile.fileName(); + tempFile.close(); + + if (!pixmap.save(tempPath, "PNG")) { + QFile::remove(tempPath); + return QString(); + } + + return tempPath; // Success +} + void ImportFTBPage::suggestCurrent() { if (!isOpened) @@ -100,16 +129,26 @@ void ImportFTBPage::suggestCurrent() dialog->setSuggestedPack(selected.name, new PackInstallTask(selected)); QString editedLogoName = QString("ftb_%1_%2.jpg").arg(selected.name, QString::number(selected.id)); - dialog->setSuggestedIconFromFile(FS::PathCombine(selected.path, "folder.jpg"), editedLogoName); + auto iconPath = FS::PathCombine(selected.path, "folder.jpg"); + if (!QFileInfo::exists(iconPath)) { + // need to save the icon as that actual logo is not a image on the disk + iconPath = saveIconToTempFile(selected.icon); + } + if (!iconPath.isEmpty() && QFileInfo::exists(iconPath)) { + dialog->setSuggestedIconFromFile(iconPath, editedLogoName); + } } -void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex) { if (!now.isValid()) { onPackSelectionChanged(); return; } - Modpack selectedPack = currentModel->data(now, Qt::UserRole).value(); + + QVariant raw = currentModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 7afff5a9d..25b900f97 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -23,7 +23,6 @@ #include #include -#include #include "modplatform/import_ftb/PackHelpers.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ListModel.h" @@ -42,7 +41,7 @@ class ImportFTBPage : public QWidget, public ModpackProviderBasePage { explicit ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportFTBPage(); QString displayName() const override { return tr("FTB App Import"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } + QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "import_ftb"; } QString helpPage() const override { return "FTB-import"; } bool shouldDisplay() const override { return true; } @@ -52,7 +51,7 @@ class ImportFTBPage : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 337c3e474..bb9990650 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -19,10 +19,13 @@ - Note: If your FTB instances are not in the default location, select it using the button next to search. + Note: Many recent FTB modpacks are also available from CurseForge! Also, if your FTB instances are not in the default location, select it using the button next to search. - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter + + + true
    @@ -47,8 +50,7 @@ - - .. + true @@ -82,7 +84,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index f3c737977..1a0f8310b 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -17,7 +17,6 @@ */ #include "ListModel.h" -#include #include #include #include @@ -36,7 +35,7 @@ namespace FTBImportAPP { QString getFTBRoot() { QString partialPath = QDir::homePath(); -#if defined(Q_OS_OSX) +#if defined(Q_OS_MACOS) partialPath = FS::PathCombine(partialPath, "Library/Application Support"); #endif return FS::PathCombine(partialPath, ".ftba"); @@ -106,9 +105,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } auto pack = m_modpacks.at(pos); - if (role == Qt::ToolTipRole) { - } - switch (role) { case Qt::ToolTipRole: return tr("Minecraft %1").arg(pack.mcVersion); @@ -128,8 +124,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return tr("Minecraft %1").arg(pack.mcVersion); - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -148,8 +142,12 @@ FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (m_currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); @@ -171,7 +169,9 @@ bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unuse return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); } @@ -214,4 +214,4 @@ QString ListModel::getUserPath() path = m_instances_path; return path; } -} // namespace FTBImportAPP \ No newline at end of file +} // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index a842ac8ff..72628465f 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -67,4 +67,4 @@ class ListModel : public QAbstractListModel { ModpackList m_modpacks; const QString m_instances_path; }; -} // namespace FTBImportAPP \ No newline at end of file +} // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 98922123c..eb95b291c 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -61,8 +61,12 @@ FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); @@ -84,7 +88,9 @@ bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unuse return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); if (searchTerm.startsWith("#")) return pack.packCode == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); @@ -167,7 +173,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.logo)) { return (m_logoMap.value(pack.logo)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logo); return icon; } @@ -195,8 +201,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -213,7 +217,7 @@ void ListModel::fill(ModpackList modpacks_) endResetModel(); } -void ListModel::addPack(Modpack modpack) +void ListModel::addPack(const Modpack& modpack) { beginResetModel(); this->modpacks.append(modpack); @@ -268,7 +272,7 @@ void ListModel::requestLogo(QString file) job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] { + connect(job, &NetJob::finished, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { @@ -276,7 +280,7 @@ void ListModel::requestLogo(QString file) } }); - QObject::connect(job, &NetJob::failed, this, [this, file, job] { + connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h index f35012078..e4477c929 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -62,7 +62,7 @@ class ListModel : public QAbstractListModel { Qt::ItemFlags flags(const QModelIndex& index) const override; void fill(ModpackList modpacks); - void addPack(Modpack modpack); + void addPack(const Modpack& modpack); void clear(); void remove(int row); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 226a30ee3..1576f52fe 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -213,7 +213,7 @@ void Page::ftbPackDataDownloadAborted() CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); } -void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) +void Page::ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack) { privateListModel->addPack(pack); } @@ -233,7 +233,9 @@ void Page::onPublicPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModel onPackSelectionChanged(); return; } - Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = publicFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -243,7 +245,9 @@ void Page::onThirdPartyPackSelectionChanged(QModelIndex now, [[maybe_unused]] QM onPackSelectionChanged(); return; } - Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = thirdPartyFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -253,7 +257,9 @@ void Page::onPrivatePackSelectionChanged(QModelIndex now, [[maybe_unused]] QMode onPackSelectionChanged(); return; } - Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = privateFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -328,7 +334,9 @@ void Page::onTabChanged(int tab) currentList->selectionModel()->reset(); QModelIndex idx = currentList->currentIndex(); if (idx.isValid()) { - auto pack = currentModel->data(idx, Qt::UserRole).value(); + QVariant raw = currentModel->data(idx, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); onPackSelectionChanged(&pack); } else { onPackSelectionChanged(); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index a2dee24e9..db70ae79e 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -39,7 +39,6 @@ #include #include -#include #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" @@ -64,7 +63,7 @@ class Page : public QWidget, public ModpackProviderBasePage { explicit Page(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~Page(); QString displayName() const override { return "FTB Legacy"; } - QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } + QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "legacy_ftb"; } QString helpPage() const override { return "FTB-legacy"; } bool shouldDisplay() const override; @@ -74,7 +73,7 @@ class Page : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); @@ -85,7 +84,7 @@ class Page : public QWidget, public ModpackProviderBasePage { void ftbPackDataDownloadFailed(QString reason); void ftbPackDataDownloadAborted(); - void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); + void ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); void onSortingSelectionChanged(QString data); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 417ff4080..05cd2970c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -36,8 +36,10 @@ #include "ModrinthModel.h" +#include "Application.h" #include "BuildConfig.h" #include "Json.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "net/NetJob.h" #include "ui/widgets/ProjectItem.h" @@ -45,6 +47,7 @@ #include "net/ApiDownload.h" #include +#include namespace Modrinth { @@ -61,7 +64,7 @@ void ModpackListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if (nextSearchOffset == 0) { + if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -71,27 +74,27 @@ void ModpackListModel::fetchMore(const QModelIndex& parent) auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } - Modrinth::Modpack pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { - if (pack.description.length() > 100) { + if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); + QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } - return pack.description; + return pack->description; } case Qt::DecorationRole: { - if (m_logoMap.contains(pack.iconName)) - return m_logoMap.value(pack.iconName); + if (m_logoMap.contains(pack->logoName)) + return m_logoMap.value(pack->logoName); - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); + ((ModpackListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { @@ -103,11 +106,9 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return QSize(0, 58); // Custom data case UserDataTypes::TITLE: - return pack.name; + return pack->name; case UserDataTypes::DESCRIPTION: - return pack.description; - case UserDataTypes::SELECTED: - return false; + return pack->description; case UserDataTypes::INSTALLED: return false; default: @@ -120,10 +121,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; - modpacks[pos] = value.value(); + m_modpacks[pos] = value.value(); return true; } @@ -132,67 +133,62 @@ void ModpackListModel::performPaginatedSearch() { if (hasActiveSearchJob()) return; + static const ModrinthAPI api; - if (currentSearchTerm.startsWith("#")) { - auto projectId = currentSearchTerm.mid(1); + if (m_currentSearchTerm.startsWith("#")) { + auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; - static const ModrinthAPI api; - if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { - jobPtr = job; - jobPtr->start(); + auto project = std::make_shared(); + project->addonId = projectId; + if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { + m_jobPtr = job; + m_jobPtr->start(); } return; } } // TODO: Move to standalone API ResourceAPI::SortingMethod sort{}; - sort.name = currentSort; - auto searchUrl = ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, - m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); + sort.name = m_currentSort; - auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); + ResourceAPI::Callback> callbacks{}; - QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { - QJsonParseError parseError{}; + callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_abort = [this] { + qCritical() << "Search task aborted by an unknown reason!"; + searchRequestFailed("Aborted"); + }; - QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); - if (parseError.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset - << " reason: " << parseError.errorString(); - qWarning() << *m_allResponse; - return; - } + auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, + std::move(callbacks)); - searchRequestFinished(doc); - }); - QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); - - jobPtr = netJob; - jobPtr->start(); + m_jobPtr = netJob; + m_jobPtr->start(); } void ModpackListModel::refresh() { if (hasActiveSearchJob()) { - jobPtr->abort(); - searchState = ResetRequested; + m_jobPtr->abort(); + m_searchState = ResetRequested; return; } beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - searchState = None; + m_searchState = None; - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } @@ -223,12 +219,12 @@ void ModpackListModel::searchWithTerm(const QString& term, auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) { + if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort_str && !filterChanged) { return; } - currentSearchTerm = term; - currentSort = sort_str; + m_currentSearchTerm = term; + m_currentSort = sort_str; m_filter = filter; refresh(); @@ -255,15 +251,15 @@ void ModpackListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); + if (m_waitingCallbacks.contains(logo)) { + m_waitingCallbacks.value(logo)(fullPath); } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); @@ -278,8 +274,8 @@ void ModpackListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); - for (int i = 0; i < modpacks.size(); i++) { - if (modpacks[i].iconName == logo) { + for (int i = 0; i < m_modpacks.size(); i++) { + if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } @@ -291,65 +287,38 @@ void ModpackListModel::logoFailed(QString logo) m_loadingLogos.removeAll(logo); } -void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) +void ModpackListModel::searchRequestFinished(QList& newList) { - jobPtr.reset(); - - QList newList; - - auto packs_all = doc_all.object().value("hits").toArray(); - for (auto packRaw : packs_all) { - auto packObj = packRaw.toObject(); + m_jobPtr.reset(); - Modrinth::Modpack pack; - try { - Modrinth::loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); - continue; - } - } - - if (packs_all.size() < m_modpacks_per_page) { - searchState = Finished; + if (newList.size() < m_modpacks_per_page) { + m_searchState = Finished; } else { - nextSearchOffset += m_modpacks_per_page; - searchState = CanPossiblyFetchMore; + m_nextSearchOffset += m_modpacks_per_page; + m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); + m_modpacks.append(newList); endInsertRows(); } -void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { - jobPtr.reset(); - - auto packObj = doc.object(); - - Modrinth::Modpack pack; - try { - Modrinth::loadIndexedPack(pack, packObj); - pack.id = Json::ensureString(packObj, "id", pack.id); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); - return; - } + m_jobPtr.reset(); - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); - modpacks.append({ pack }); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); + m_modpacks.append(pack); endInsertRows(); } -void ModpackListModel::searchRequestFailed(QString reason) +void ModpackListModel::searchRequestFailed(QString) { - auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); + auto failed_action = dynamic_cast(m_jobPtr.get())->getFailedActions().at(0); if (failed_action->replyStatusCode() == -1) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); @@ -361,17 +330,17 @@ void ModpackListModel::searchRequestFailed(QString reason) .arg(m_parent->displayName()) .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); } - jobPtr.reset(); + m_jobPtr.reset(); - if (searchState == ResetRequested) { + if (m_searchState == ResetRequested) { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } else { - searchState = Finished; + m_searchState = Finished; } } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 640ddf688..114c07ca6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -37,7 +37,7 @@ #include -#include "modplatform/modrinth/ModrinthPackManifest.h" +#include "modplatform/ModIndex.h" #include "net/NetJob.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" @@ -56,7 +56,7 @@ class ModpackListModel : public QAbstractListModel { ModpackListModel(ModrinthPage* parent); ~ModpackListModel() override = default; - inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : m_modpacks.size(); }; inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; @@ -66,27 +66,27 @@ class ModpackListModel : public QAbstractListModel { auto data(const QModelIndex& index, int role) const -> QVariant override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + inline void setActiveJob(NetJob::Ptr ptr) { m_jobPtr = ptr; } /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline auto canFetchMore(const QModelIndex& parent) const -> bool override { - return parent.isValid() ? false : searchState == CanPossiblyFetchMore; + return parent.isValid() ? false : m_searchState == CanPossiblyFetchMore; }; public slots: - void searchRequestFinished(QJsonDocument& doc_all); + void searchRequestFinished(QList& doc_all); void searchRequestFailed(QString reason); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); protected slots: @@ -103,20 +103,20 @@ class ModpackListModel : public QAbstractListModel { protected: ModrinthPage* m_parent; - QList modpacks; + QList m_modpacks; LogoMap m_logoMap; - QMap waitingCallbacks; + QMap m_waitingCallbacks; QStringList m_failedLogos; QStringList m_loadingLogos; - QString currentSearchTerm; - QString currentSort; + QString m_currentSearchTerm; + QString m_currentSort; std::shared_ptr m_filter; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + int m_nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; - Task::Ptr jobPtr; + Task::Ptr m_jobPtr; std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 7d70abec4..ab4dcc4c6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -36,6 +36,7 @@ #include "ModrinthPage.h" #include "Version.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" @@ -57,17 +58,17 @@ #include ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) - : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) + : QWidget(parent), m_ui(new Ui::ModrinthPage), m_dialog(dialog), m_fetch_progress(this, false) { - ui->setupUi(this); + m_ui->setupUi(this); createFilterWidget(); - ui->searchEdit->installEventFilter(this); + m_ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); - ui->packView->setModel(m_model); + m_ui->packView->setModel(m_model); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); @@ -78,30 +79,30 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->verticalLayout->insertWidget(1, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); - ui->sortByBox->addItem(tr("Sort by Relevance")); - ui->sortByBox->addItem(tr("Sort by Total Downloads")); - ui->sortByBox->addItem(tr("Sort by Follows")); - ui->sortByBox->addItem(tr("Sort by Newest")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); + m_ui->sortByBox->addItem(tr("Sort by Relevance")); + m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); + m_ui->sortByBox->addItem(tr("Sort by Follows")); + m_ui->sortByBox->addItem(tr("Sort by Newest")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); - connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ModrinthPage::onVersionSelectionChanged); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged); - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packDescription->setMetaEntry(metaEntryBase()); + m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthPage::~ModrinthPage() { - delete ui; + delete m_ui; } void ModrinthPage::retranslate() { - ui->retranslateUi(this); + m_ui->retranslateUi(this); } void ModrinthPage::openedImpl() @@ -113,7 +114,7 @@ void ModrinthPage::openedImpl() bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { auto* keyEvent = reinterpret_cast(event); if (keyEvent->key() == Qt::Key_Return) { this->triggerSearch(); @@ -129,150 +130,106 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr filter) -{ - if (!filter) - return true; - return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders - (filter->releases.empty() || // releases - std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && - filter->checkMcVersions({ v.gameVersion })); // gameVersion} -} - void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { - ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); } return; } - current = m_model->data(curr, Qt::UserRole).value(); - auto name = current.name; + m_current = m_model->data(curr, Qt::UserRole).value(); + auto name = m_current->name; - if (!current.extraInfoLoaded) { + if (!m_current->extraDataLoaded) { qDebug() << "Loading modrinth modpack information"; - - auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - - QString id = current.id; - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { - if (id != current.id) { + ResourceAPI::Callback callbacks; + + auto id = m_current->addonId; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + callbacks.on_succeed = [this, id, curr](auto& pack) { + if (id != m_current->addonId) { return; // wrong request? } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - auto obj = Json::requireObject(doc); - - try { - Modrinth::loadIndexedInfo(current, obj); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading modrinth modpack version: " << e.cause(); - } - - updateUI(); - QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(pack); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache extra info for the current pack!"; suggestCurrent(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - netJob->start(); + updateUI(); + }; + if (auto netJob = m_api.getProjectInfo({ m_current }, std::move(callbacks)); netJob) { + m_job = netJob; + m_job->start(); + } + } else updateUI(); - if (!current.versionsLoaded || m_filterWidget->changed()) { + if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; - auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - - QString id = current.id; - - netJob->addNetAction( - Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); + ResourceAPI::Callback> callbacks{}; - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { - if (id != current.id) { - return; // wrong request? - } - - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; + auto addonId = m_current->addonId; + // Use default if no callbacks are set + callbacks.on_succeed = [this, curr, addonId](auto& doc) { + if (addonId != m_current->addonId) { + return; // wrong request } - try { - Modrinth::loadIndexedVersions(current, doc); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading modrinth modpack version: " << e.cause(); - } - auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; + m_current->versions = doc; + m_current->versionsLoaded = true; + auto pred = [this](const ModPlatform::IndexedVersion& v) { + if (auto filter = m_filterWidget->getFilter()) + return !filter->checkModpackFilters(v); + return false; + }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) - current.versions.removeIf(pred); + m_current->versions.removeIf(pred); #else - for (auto it = current.versions.begin(); it != current.versions.end();) - if (pred(*it)) - it = current.versions.erase(it); - else - ++it; + for (auto it = m_current->versions.begin(); it != m_current->versions.end();) + if (pred(*it)) + it = m_current->versions.erase(it); + else + ++it; #endif - for (auto version : current.versions) { - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) - ? QString(" for %1").arg(version.gameVersion) - : ""; - auto versionStr = !version.name.contains(version.version) ? version.version : ""; - ui->versionSelectionBox->addItem(QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type), - QVariant(version.id)); + for (const auto& version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.fileId)); } QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(m_current); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; suggestCurrent(); - }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - netJob->start(); + }; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + + auto netJob = m_api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + + m_job2 = netJob; + m_job2->start(); } else { - for (auto version : current.versions) { - if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + for (auto version : m_current->versions) { + if (!version.version.contains(version.version)) + m_ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.version, version.version_number), + QVariant(version.fileId)); else - ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); + m_ui->versionSelectionBox->addItem(version.version, QVariant(version.fileId)); } suggestCurrent(); @@ -283,53 +240,64 @@ void ModrinthPage::updateUI() { QString text = ""; - if (current.extra.projectUrl.isEmpty()) - text = current.name; + if (m_current->websiteUrl.isEmpty()) + text = m_current->name; else - text = "" + current.name + ""; + text = "websiteUrl + "\">" + m_current->name + ""; - // TODO: Implement multiple authors with links - text += "
    " + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); + if (!m_current->authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) { + if (author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : m_current->authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
    " + tr(" by ") + authorStrs.join(", "); + } - if (current.extraInfoLoaded) { - if (current.extra.status == "archived") { + if (m_current->extraDataLoaded) { + if (m_current->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.extra.donate.isEmpty()) { + if (!m_current->extraData.donate.isEmpty()) { text += "

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

    " + tr("External links:") + "
    "; } - if (!current.extra.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current.extra.issuesUrl) + "
    "; - if (!current.extra.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current.extra.wikiUrl) + "
    "; - if (!current.extra.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current.extra.sourceUrl) + "
    "; - if (!current.extra.discordUrl.isEmpty()) - text += "- " + tr("Discord: %1").arg(current.extra.discordUrl) + "
    "; + if (!m_current->extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; + if (!m_current->extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; + if (!m_current->extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; + if (!m_current->extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: %1").arg(m_current->extraData.discordUrl) + "
    "; } text += "
    "; - text += markdownToHTML(current.extra.body.toUtf8()); + text += markdownToHTML(m_current->extraData.body.toUtf8()); - ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); - ui->packDescription->flush(); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); + m_ui->packDescription->flush(); } void ModrinthPage::suggestCurrent() @@ -338,21 +306,21 @@ void ModrinthPage::suggestCurrent() return; } - if (selectedVersion.isEmpty()) { - dialog->setSuggestedPack(); + if (m_selectedVersion.isEmpty()) { + m_dialog->setSuggestedPack(); return; } - for (auto& ver : current.versions) { - if (ver.id == selectedVersion) { + for (auto& ver : m_current->versions) { + if (ver.fileId == m_selectedVersion) { QMap extra_info; - extra_info.insert("pack_id", current.id); - extra_info.insert("pack_version_id", ver.id); + extra_info.insert("pack_id", m_current->addonId.toString()); + extra_info.insert("pack_version_id", ver.fileId.toString()); - dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info))); - auto iconName = current.iconName; - m_model->getLogo(iconName, current.iconUrl.toString(), - [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); + m_dialog->setSuggestedPack(m_current->name, ver.version, new InstanceImportTask(ver.downloadUrl, this, std::move(extra_info))); + QString editedLogoName = "modrinth_" + m_current->logoName; + m_model->getLogo(m_current->logoName, m_current->logoUrl, + [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); break; } @@ -361,52 +329,53 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { - ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), m_filterWidget->changed()); + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + bool filterChanged = m_filterWidget->changed(); + m_model->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(m_model->activeSearchJob().get()); } void ModrinthPage::onVersionSelectionChanged(int index) { if (index == -1) { - selectedVersion = ""; + m_selectedVersion = ""; return; } - selectedVersion = ui->versionSelectionBox->itemData(index).toString(); + m_selectedVersion = m_ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } void ModrinthPage::setSearchTerm(QString term) { - ui->searchEdit->setText(term); + m_ui->searchEdit->setText(term); } QString ModrinthPage::getSerachTerm() const { - return ui->searchEdit->text(); + return m_ui->searchEdit->text(); } void ModrinthPage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, true, this); + auto widget = ModFilterWidget::create(nullptr, true); m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } - connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); - m_categoriesTask = ModrinthAPI::getModCategories(response); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { - auto categories = ModrinthAPI::loadCategories(response, "modpack"); + m_categoriesTask = ModrinthAPI::getModCategories(response.get()); + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadCategories(response.get(), "modpack"); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); -} \ No newline at end of file +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 7f504cdbd..4ca41a3e0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -36,10 +36,10 @@ #pragma once -#include "Application.h" +#include "modplatform/ModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" @@ -63,14 +63,14 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { ~ModrinthPage() override; QString displayName() const override { return tr("Modrinth"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } + QIcon icon() const override { return QIcon::fromTheme("modrinth"); } QString id() const override { return "modrinth"; } QString helpPage() const override { return "Modrinth-platform"; } - inline auto debugName() const -> QString { return "Modrinth"; } - inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; }; + inline QString debugName() const { return "Modrinth"; } + inline QString metaEntryBase() const { return "ModrinthModpacks"; }; - auto getCurrent() -> Modrinth::Modpack& { return current; } + ModPlatform::IndexedPack::Ptr getCurrent() { return m_current; } void suggestCurrent(); void updateUI(); @@ -82,7 +82,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); @@ -91,18 +91,22 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { void createFilterWidget(); private: - Ui::ModrinthPage* ui; - NewInstanceDialog* dialog; + Ui::ModrinthPage* m_ui; + NewInstanceDialog* m_dialog; Modrinth::ModpackListModel* m_model; - Modrinth::Modpack current; - QString selectedVersion; + ModPlatform::IndexedPack::Ptr m_current; + QString m_selectedVersion; ProgressWidget m_fetch_progress; // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; + + ModrinthAPI m_api; + Task::Ptr m_job; + Task::Ptr m_job2; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp deleted file mode 100644 index 856018294..000000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "ModrinthResourceModels.h" - -#include "modplatform/modrinth/ModrinthAPI.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" - -namespace ResourceDownload { - -ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {} - -void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); -} - -auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion -{ - return ::Modrinth::loadDependencyVersions(m, arr, &m_base_instance); -} - -auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {} - -void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); -} - -auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {} - -void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); -} - -auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {} - -void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); -} - -auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h deleted file mode 100644 index 15cd58544..000000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/ResourcePackModel.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" - -namespace ResourceDownload { - -class ModrinthModModel : public ModModel { - Q_OBJECT - - public: - ModrinthModModel(BaseInstance&); - ~ModrinthModModel() override = default; - - private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthResourcePackModel : public ResourcePackResourceModel { - Q_OBJECT - - public: - ModrinthResourcePackModel(const BaseInstance&); - ~ModrinthResourcePackModel() override = default; - - private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthTexturePackModel : public TexturePackResourceModel { - Q_OBJECT - - public: - ModrinthTexturePackModel(const BaseInstance&); - ~ModrinthTexturePackModel() override = default; - - private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthShaderPackModel : public ShaderPackResourceModel { - Q_OBJECT - - public: - ModrinthShaderPackModel(const BaseInstance&); - ~ModrinthShaderPackModel() override = default; - - private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 4ee620677..0646cbdc9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -37,29 +37,27 @@ */ #include "ModrinthResourcePages.h" +#include "ui/pages/modplatform/DataPackModel.h" #include "ui_ResourcePage.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/ResourceDownloadDialog.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" - namespace ResourceDownload { ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new ModrinthModModel(instance); + m_model = new ModModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -68,17 +66,16 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { - m_model = new ModrinthResourcePackModel(instance); + m_model = new ResourcePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -87,17 +84,16 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : TexturePackResourcePage(dialog, instance) { - m_model = new ModrinthTexturePackModel(instance); + m_model = new TexturePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -106,22 +102,38 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { - m_model = new ModrinthShaderPackModel(instance); + m_model = new ShaderPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } +ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) +{ + m_model = new DataPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... @@ -141,20 +153,24 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; } +auto ModrinthDataPackPage::shouldDisplay() const -> bool +{ + return true; +} -unique_qobject_ptr ModrinthModPage::createFilterWidget() +std::unique_ptr ModrinthModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), true, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), true); } void ModrinthModPage::prepareProviderCategories() { auto response = std::make_shared(); - auto task = ModrinthAPI::getModCategories(response); - QObject::connect(task.get(), &Task::succeeded, [this, response]() { - auto categories = ModrinthAPI::loadModCategories(response); + m_categoriesTask = ModrinthAPI::getModCategories(response.get()); + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadModCategories(response.get()); m_filter_widget->setCategories(categories); }); - task->start(); + m_categoriesTask->start(); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index eaf6129a5..3f41a3d5e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -38,10 +38,9 @@ #pragma once -#include "Application.h" - #include "modplatform/ResourceAPI.h" +#include "ui/pages/modplatform/DataPackPage.h" #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/ShaderPackPage.h" @@ -56,7 +55,7 @@ static inline QString displayName() } static inline QIcon icon() { - return APPLICATION->getThemedIcon("modrinth"); + return QIcon::fromTheme("modrinth"); } static inline QString id() { @@ -84,20 +83,21 @@ class ModrinthModPage : public ModPage { ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~ModrinthModPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } - unique_qobject_ptr createFilterWidget() override; + std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; + Task::Ptr m_categoriesTask; }; class ModrinthResourcePackPage : public ResourcePackResourcePage { @@ -112,15 +112,15 @@ class ModrinthResourcePackPage : public ResourcePackResourcePage { ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthResourcePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; class ModrinthTexturePackPage : public TexturePackResourcePage { @@ -135,15 +135,15 @@ class ModrinthTexturePackPage : public TexturePackResourcePage { ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthTexturePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; class ModrinthShaderPackPage : public ShaderPackResourcePage { @@ -158,15 +158,38 @@ class ModrinthShaderPackPage : public ShaderPackResourcePage { ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthShaderPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; + + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + inline auto helpPage() const -> QString override { return ""; } +}; + +class ModrinthDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static ModrinthDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthDataPackPage() override = default; + + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index fc7fa4d39..1049d1f2e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -36,8 +36,8 @@ #pragma once #include +#include #include -#include namespace Technic { struct Modpack { @@ -61,7 +61,7 @@ struct Modpack { bool versionsLoaded = false; QString recommended; - QVector versions; + QList versions; }; } // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index f7e7f4433..2de280907 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -71,7 +71,7 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; } @@ -89,8 +89,6 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: @@ -158,11 +156,11 @@ void Technic::ListModel::performSearch() if (!clientId.isEmpty()) { searchUrl += "?cid=" + clientId; } - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response.get())); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); + connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void Technic::ListModel::searchRequestFinished() @@ -193,7 +191,7 @@ void Technic::ListModel::searchRequestFinished() if (pack.slug == "vanilla") continue; - auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + auto rawURL = technicPackObject["iconUrl"].toString("null"); if (rawURL == "null") { pack.logoUrl = "null"; pack.logoName = "null"; @@ -301,12 +299,12 @@ void Technic::ListModel::requestLogo(QString logo, QString url) auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); logoLoaded(logo, fullPath); }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index 09e9294bb..8ad8b88bb 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -58,8 +58,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } private slots: void searchRequestFinished(); @@ -86,7 +86,7 @@ class ListModel : public QAbstractListModel { Single, } searchMode = List; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + std::unique_ptr response = std::make_unique(); }; } // namespace Technic diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 50d267b1f..54d228c9c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -135,7 +135,9 @@ void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelI return; } - current = model->data(first, Qt::UserRole).value(); + QVariant raw = model->data(first, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + current = raw.value(); suggestCurrent(); } @@ -161,8 +163,8 @@ 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)); - QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { + QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response.get())); + connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); if (current.slug != slug) { @@ -200,11 +202,11 @@ void TechnicPage::suggestCurrent() } } - current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); - current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); - current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); - current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); - current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); + current.minecraftVersion = obj["minecraft"].toString(); + current.websiteUrl = obj["platformUrl"].toString(); + current.author = obj["user"].toString(); + current.description = obj["description"].toString(); + current.currentVersion = obj["version"].toString(); current.metadataLoaded = true; metadataLoaded(); @@ -258,9 +260,9 @@ 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)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response.get())); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index d1f691b22..9bde6b78c 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -38,7 +38,6 @@ #include #include -#include #include "TechnicData.h" #include "net/NetJob.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" @@ -61,7 +60,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { explicit TechnicPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~TechnicPage(); virtual QString displayName() const override { return "Technic"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("technic"); } + virtual QIcon icon() const override { return QIcon::fromTheme("technic"); } virtual QString id() const override { return "technic"; } virtual QString helpPage() const override { return "Technic-platform"; } virtual bool shouldDisplay() const override; @@ -74,7 +73,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { /** 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; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); @@ -96,7 +95,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { QString selectedVersion; NetJob::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + std::unique_ptr response = std::make_unique(); ProgressWidget m_fetch_progress; diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 8caae173c..6b8ece9f7 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -14,7 +14,7 @@ #include "JavaCommon.h" -#include "ui/widgets/JavaSettingsWidget.h" +#include "ui/widgets/JavaWizardWidget.h" #include "ui/widgets/VersionSelectWidget.h" JavaWizardPage::JavaWizardPage(QWidget* parent) : BaseWizardPage(parent) @@ -27,7 +27,7 @@ void JavaWizardPage::setupUi() setObjectName(QStringLiteral("javaPage")); QVBoxLayout* layout = new QVBoxLayout(this); - m_java_widget = new JavaSettingsWidget(this); + m_java_widget = new JavaWizardWidget(this); layout->addWidget(m_java_widget); setLayout(layout); @@ -58,13 +58,13 @@ bool JavaWizardPage::validatePage() settings->set("UserAskedAboutAutomaticJavaDownload", true); switch (result) { default: - case JavaSettingsWidget::ValidationStatus::Bad: { + case JavaWizardWidget::ValidationStatus::Bad: { return false; } - case JavaSettingsWidget::ValidationStatus::AllOK: { + case JavaWizardWidget::ValidationStatus::AllOK: { settings->set("JavaPath", m_java_widget->javaPath()); } /* fallthrough */ - case JavaSettingsWidget::ValidationStatus::JavaBad: { + case JavaWizardWidget::ValidationStatus::JavaBad: { // Memory auto s = APPLICATION->settings(); s->set("MinMemAlloc", m_java_widget->minHeapSize()); diff --git a/launcher/ui/setupwizard/JavaWizardPage.h b/launcher/ui/setupwizard/JavaWizardPage.h index 9bf9cfa2b..914630d0b 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.h +++ b/launcher/ui/setupwizard/JavaWizardPage.h @@ -2,7 +2,7 @@ #include "BaseWizardPage.h" -class JavaSettingsWidget; +class JavaWizardWidget; class JavaWizardPage : public BaseWizardPage { Q_OBJECT @@ -21,5 +21,5 @@ class JavaWizardPage : public BaseWizardPage { void retranslate() override; private: /* data */ - JavaSettingsWidget* m_java_widget = nullptr; + JavaWizardWidget* m_java_widget = nullptr; }; diff --git a/launcher/ui/setupwizard/SetupWizard.cpp b/launcher/ui/setupwizard/SetupWizard.cpp index 4e5bd1dca..f2e51ee41 100644 --- a/launcher/ui/setupwizard/SetupWizard.cpp +++ b/launcher/ui/setupwizard/SetupWizard.cpp @@ -57,7 +57,7 @@ void SetupWizard::pageChanged(int id) if (basePagePtr->wantsRefreshButton()) { setButtonLayout({ QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton }); auto customButton = button(QWizard::CustomButton1); - connect(customButton, &QAbstractButton::clicked, [&]() { + connect(customButton, &QAbstractButton::clicked, [this]() { auto basePagePtr = getCurrentBasePage(); if (basePagePtr) { basePagePtr->refresh(); diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp deleted file mode 100644 index fe11ed9ae..000000000 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include "ThemeWizardPage.h" -#include "ui_ThemeWizardPage.h" - -#include "Application.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" -#include "ui/widgets/ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) -{ - ui->setupUi(this); - - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); - - updateIcons(); - updateCat(); -} - -ThemeWizardPage::~ThemeWizardPage() -{ - delete ui; -} - -void ThemeWizardPage::updateIcons() -{ - qDebug() << "Setting Icons"; - ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); - ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); - ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); - ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); - ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); - ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); - ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); - ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); - ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); - ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); - update(); - repaint(); - parentWidget()->update(); -} - -void ThemeWizardPage::updateCat() -{ - qDebug() << "Setting Cat"; - ui->catImagePreviewButton->setIcon(QIcon(QString(R"(%1)").arg(APPLICATION->themeManager()->getCatPack()))); -} - -void ThemeWizardPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index f3d40b6d8..ef4613e41 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -17,27 +17,30 @@ */ #pragma once +#include +#include #include #include "BaseWizardPage.h" -namespace Ui { -class ThemeWizardPage; -} - class ThemeWizardPage : public BaseWizardPage { Q_OBJECT public: - explicit ThemeWizardPage(QWidget* parent = nullptr); - ~ThemeWizardPage(); + ThemeWizardPage(QWidget* parent = nullptr) : BaseWizardPage(parent) + { + auto layout = new QVBoxLayout(this); + layout->addWidget(&widget); + layout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); - bool validatePage() override { return true; }; - void retranslate() override; + setTitle(tr("Appearance")); + setSubTitle(tr("Select theme and icons to use")); + } - private slots: - void updateIcons(); - void updateCat(); + bool validatePage() override { return true; }; + void retranslate() override { widget.retranslateUi(); } private: - Ui::ThemeWizardPage* ui; + AppearanceWidget widget{ true }; }; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui deleted file mode 100644 index 01394ea40..000000000 --- a/launcher/ui/setupwizard/ThemeWizardPage.ui +++ /dev/null @@ -1,371 +0,0 @@ - - - ThemeWizardPage - - - - 0 - 0 - 510 - 552 - - - - WizardPage - - - - - - Select the Theme you wish to use - - - - - - - - 0 - 100 - - - - - - - - Hint: The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - true - - - - - - - Qt::Horizontal - - - - - - - Preview: - - - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - - - 0 - 256 - - - - The cat appears in the background and does not serve a purpose, it is purely visual. - - - - - - - 256 - 256 - - - - true - - - - - - - Qt::Vertical - - - - 20 - 193 - - - - - - - - - ThemeCustomizationWidget - QWidget -
    ui/widgets/ThemeCustomizationWidget.h
    -
    -
    - - -
    diff --git a/launcher/ui/themes/CatPack.cpp b/launcher/ui/themes/CatPack.cpp index 85eb85a18..7d39c8c05 100644 --- a/launcher/ui/themes/CatPack.cpp +++ b/launcher/ui/themes/CatPack.cpp @@ -43,7 +43,7 @@ #include "FileSystem.h" #include "Json.h" -QString BasicCatPack::path() +QString BasicCatPack::path() const { const auto now = QDate::currentDate(); const auto birthday = QDate(now.year(), 11, 1); @@ -63,12 +63,12 @@ QString BasicCatPack::path() JsonCatPack::PartialDate partialDate(QJsonObject date) { - auto month = Json::ensureInteger(date, "month", 1); + auto month = date["month"].toInt(1); if (month > 12) month = 12; else if (month <= 0) month = 1; - auto day = Json::ensureInteger(date, "day", 1); + auto day = date["day"].toInt(1); if (day > 31) day = 31; else if (day <= 0) @@ -83,9 +83,9 @@ JsonCatPack::JsonCatPack(QFileInfo& manifestInfo) : BasicCatPack(manifestInfo.di const auto root = doc.object(); m_name = Json::requireString(root, "name", "Catpack name"); m_default_path = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat")); - auto variants = Json::ensureArray(root, "variants", QJsonArray(), "Catpack Variants"); + auto variants = root["variants"].toArray(); for (auto v : variants) { - auto variant = Json::ensureObject(v, QJsonObject(), "Cat variant"); + auto variant = v.toObject(); m_variants << Variant{ FS::PathCombine(path, Json::requireString(variant, "path", "Variant path")), partialDate(Json::requireObject(variant, "startTime", "Variant startTime")), partialDate(Json::requireObject(variant, "endTime", "Variant endTime")) }; @@ -100,12 +100,12 @@ QDate ensureDay(int year, int month, int day) return QDate(year, month, day); } -QString JsonCatPack::path() +QString JsonCatPack::path() const { return path(QDate::currentDate()); } -QString JsonCatPack::path(QDate now) +QString JsonCatPack::path(QDate now) const { for (auto var : m_variants) { QDate startDate = ensureDay(now.year(), var.startTime.month, var.startTime.day); diff --git a/launcher/ui/themes/CatPack.h b/launcher/ui/themes/CatPack.h index 5a13d0cef..e0e34f86e 100644 --- a/launcher/ui/themes/CatPack.h +++ b/launcher/ui/themes/CatPack.h @@ -43,18 +43,18 @@ class CatPack { public: virtual ~CatPack() {} - virtual QString id() = 0; - virtual QString name() = 0; - virtual QString path() = 0; + virtual QString id() const = 0; + virtual QString name() const = 0; + virtual QString path() const = 0; }; class BasicCatPack : public CatPack { public: BasicCatPack(QString id, QString name) : m_id(id), m_name(name) {} BasicCatPack(QString id) : BasicCatPack(id, id) {} - virtual QString id() override { return m_id; } - virtual QString name() override { return m_name; } - virtual QString path() override; + virtual QString id() const override { return m_id; } + virtual QString name() const override { return m_name; } + virtual QString path() const override; protected: QString m_id; @@ -65,7 +65,7 @@ class FileCatPack : public BasicCatPack { public: FileCatPack(QString id, QFileInfo& fileInfo) : BasicCatPack(id), m_path(fileInfo.absoluteFilePath()) {} FileCatPack(QFileInfo& fileInfo) : FileCatPack(fileInfo.baseName(), fileInfo) {} - virtual QString path() { return m_path; } + virtual QString path() const { return m_path; } private: QString m_path; @@ -83,8 +83,8 @@ class JsonCatPack : public BasicCatPack { PartialDate endTime; }; JsonCatPack(QFileInfo& manifestInfo); - virtual QString path() override; - QString path(QDate now); + virtual QString path() const override; + QString path(QDate now) const; private: QString m_default_path; diff --git a/launcher/ui/themes/CatPainter.cpp b/launcher/ui/themes/CatPainter.cpp new file mode 100644 index 000000000..7c152fdc9 --- /dev/null +++ b/launcher/ui/themes/CatPainter.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui/themes/CatPainter.h" +#include +#include "Application.h" + +CatPainter::CatPainter(const QString& path, QObject* parent) : QObject(parent) +{ + // Attempt to load as a movie + m_movie = new QMovie(path, QByteArray(), this); + if (m_movie->isValid()) { + // Start the animation if it's a valid movie file + connect(m_movie, &QMovie::frameChanged, this, &CatPainter::updateFrame); + m_movie->start(); + } else { + // Otherwise, load it as a static image + delete m_movie; + m_movie = nullptr; + + m_image = QPixmap(path); + } +} + +void CatPainter::paint(QPainter* painter, const QRect& viewport) +{ + QPixmap frame = m_image; + if (m_movie && m_movie->isValid()) { + frame = m_movie->currentPixmap(); + } + + auto fit = APPLICATION->settings()->get("CatFit").toString(); + painter->setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); + int widWidth = viewport.width(); + int widHeight = viewport.height(); + auto aspectMode = Qt::IgnoreAspectRatio; + if (fit == "fill") { + aspectMode = Qt::KeepAspectRatio; + } else if (fit == "fit") { + aspectMode = Qt::KeepAspectRatio; + if (frame.width() < widWidth) + widWidth = frame.width(); + if (frame.height() < widHeight) + widHeight = frame.height(); + } + auto pixmap = frame.scaled(widWidth, widHeight, aspectMode, Qt::SmoothTransformation); + QRect rectOfPixmap = pixmap.rect(); + rectOfPixmap.moveBottomRight(viewport.bottomRight()); + painter->drawPixmap(rectOfPixmap.topLeft(), pixmap); + painter->setOpacity(1.0); +}; diff --git a/launcher/ui/pages/global/EnvironmentVariablesPage.h b/launcher/ui/themes/CatPainter.h similarity index 57% rename from launcher/ui/pages/global/EnvironmentVariablesPage.h rename to launcher/ui/themes/CatPainter.h index 6e80775ec..c36cb7617 100644 --- a/launcher/ui/pages/global/EnvironmentVariablesPage.h +++ b/launcher/ui/themes/CatPainter.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2023 TheKodeToad + * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,25 +18,22 @@ #pragma once -#include +#include +#include +#include +#include -#include "ui/pages/BasePage.h" -#include "ui/widgets/EnvironmentVariables.h" - -class EnvironmentVariablesPage : public QWidget, public BasePage { +class CatPainter : public QObject { Q_OBJECT - public: - explicit EnvironmentVariablesPage(QWidget* parent = nullptr); - - QString displayName() const override; - QIcon icon() const override; - QString id() const override; - QString helpPage() const override; + CatPainter(const QString& path, QObject* parent = nullptr); + virtual ~CatPainter() = default; + void paint(QPainter*, const QRect&); - bool apply() override; - void retranslate() override; + signals: + void updateFrame(); private: - EnvironmentVariables* variables; + QMovie* m_movie = nullptr; + QPixmap m_image; }; diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 081ba1900..e32e53381 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -179,10 +179,10 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) const QJsonObject root = doc.object(); m_name = Json::requireString(root, "name", "Theme name"); m_widgets = Json::requireString(root, "widgets", "Qt widget theme"); - m_qssFilePath = Json::ensureString(root, "qssFilePath", "themeStyle.css"); + m_qssFilePath = root["qssFilePath"].toString("themeStyle.css"); - auto readColor = [&](const QJsonObject& colors, const QString& colorName) -> QColor { - auto colorValue = Json::ensureString(colors, colorName, QString()); + auto readColor = [](const QJsonObject& colors, const QString& colorName) -> QColor { + auto colorValue = colors[colorName].toString(); if (!colorValue.isEmpty()) { QColor color(colorValue); if (!color.isValid()) { @@ -196,7 +196,7 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) if (root.contains("colors")) { auto colorsRoot = Json::requireObject(root, "colors"); - auto readAndSetPaletteColor = [&](QPalette::ColorRole role, const QString& colorName) { + auto readAndSetPaletteColor = [this, readColor, colorsRoot](QPalette::ColorRole role, const QString& colorName) { auto color = readColor(colorsRoot, colorName); if (color.isValid()) { m_palette.setColor(role, color); @@ -222,14 +222,14 @@ bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) // fade m_fadeColor = readColor(colorsRoot, "fadeColor"); - m_fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); + m_fadeAmount = colorsRoot["fadeAmount"].toDouble(0.5); } if (root.contains("logColors")) { hasCustomLogColors = true; auto logColorsRoot = Json::requireObject(root, "logColors"); - auto readAndSetLogColor = [&](MessageLevel::Enum level, bool fg, const QString& colorName) { + auto readAndSetLogColor = [this, readColor, logColorsRoot](MessageLevel level, bool fg, const QString& colorName) { auto color = readColor(logColorsRoot, colorName); if (color.isValid()) { if (fg) diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index 80e821349..6d95cc7dd 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -18,6 +18,11 @@ #include "HintOverrideProxyStyle.h" +HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) +{ + setObjectName(baseStyle()->objectName()); +} + int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, @@ -26,5 +31,11 @@ int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) return 0; + if (hint == QStyle::SH_Slider_AbsoluteSetButtons) + return Qt::LeftButton | Qt::MiddleButton; + + if (hint == QStyle::SH_Slider_PageSetButtons) + return Qt::RightButton; + return QProxyStyle::styleHint(hint, option, widget, returnData); } diff --git a/launcher/ui/themes/HintOverrideProxyStyle.h b/launcher/ui/themes/HintOverrideProxyStyle.h index 09b296018..e9c489d09 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.h +++ b/launcher/ui/themes/HintOverrideProxyStyle.h @@ -25,7 +25,7 @@ class HintOverrideProxyStyle : public QProxyStyle { Q_OBJECT public: - HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {} + explicit HintOverrideProxyStyle(QStyle* style); int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index 7dc5fc64a..6e0e613f9 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -42,11 +42,12 @@ class QStyle; struct LogColors { - QMap background; - QMap foreground; + QMap background; + QMap foreground; }; // TODO: rename to Theme; this is not an interface as it contains method implementations +// TODO: make methods const class ITheme { public: virtual ~ITheme() {} diff --git a/launcher/ui/themes/IconTheme.cpp b/launcher/ui/themes/IconTheme.cpp index 4bd889854..6415c5148 100644 --- a/launcher/ui/themes/IconTheme.cpp +++ b/launcher/ui/themes/IconTheme.cpp @@ -21,8 +21,6 @@ #include #include -IconTheme::IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} - bool IconTheme::load() { const QString path = m_path + "/index.theme"; @@ -36,18 +34,3 @@ bool IconTheme::load() settings.endGroup(); return !m_name.isNull(); } - -QString IconTheme::id() -{ - return m_id; -} - -QString IconTheme::path() -{ - return m_path; -} - -QString IconTheme::name() -{ - return m_name; -} diff --git a/launcher/ui/themes/IconTheme.h b/launcher/ui/themes/IconTheme.h index 4e466c6ae..f49e39289 100644 --- a/launcher/ui/themes/IconTheme.h +++ b/launcher/ui/themes/IconTheme.h @@ -22,13 +22,13 @@ class IconTheme { public: - IconTheme(const QString& id, const QString& path); + IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} IconTheme() = default; bool load(); - QString id(); - QString path(); - QString name(); + QString id() const { return m_id; } + QString path() const { return m_path; } + QString name() const { return m_name; } private: QString m_id; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index 7fba08026..c9b2e5cfd 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -54,7 +54,7 @@ SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalett m_colorPalette = defaultPalette; } else { auto style = QStyleFactory::create(styleName); - m_colorPalette = style->standardPalette(); + m_colorPalette = style != nullptr ? style->standardPalette() : defaultPalette; delete style; } } diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 30a1fe7be..6e22c5b76 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -50,6 +50,11 @@ ThemeManager::ThemeManager() initializeCatPacks(); } +ThemeManager::~ThemeManager() +{ + stopSettingNewWindowColorsOnMac(); +} + /// @brief Adds the Theme to the list of themes /// @param theme The Theme to add /// @return Theme ID @@ -174,6 +179,12 @@ void ThemeManager::initializeWidgets() themeDebugLog() << "<> Widget themes initialized."; } +#ifndef Q_OS_MACOS +void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) {} +void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) {} +void ThemeManager::stopSettingNewWindowColorsOnMac() {} +#endif + QList ThemeManager::getValidIconThemes() { QList ret; @@ -247,6 +258,7 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); + setTitlebarColorOfAllWindowsOnMac(qApp->palette().window().color()); m_logColors = theme->logColorScheme(); } else { diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 8de7562d1..fe0fea926 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -27,18 +27,17 @@ #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" -inline auto themeDebugLog() -{ +inline auto themeDebugLog() { return qDebug() << "[Theme]"; } -inline auto themeWarningLog() -{ +inline auto themeWarningLog() { return qWarning() << "[Theme]"; } class ThemeManager { public: ThemeManager(); + ~ThemeManager(); QList getValidIconThemes(); QList getValidApplicationThemes(); @@ -64,9 +63,9 @@ class ThemeManager { private: std::map> m_themes; std::map m_icons; - QDir m_iconThemeFolder{ "iconthemes" }; - QDir m_applicationThemeFolder{ "themes" }; - QDir m_catPacksFolder{ "catpacks" }; + QDir m_iconThemeFolder{"iconthemes"}; + QDir m_applicationThemeFolder{"themes"}; + QDir m_catPacksFolder{"catpacks"}; std::map> m_catPacks; QPalette m_defaultPalette; QString m_defaultStyle; @@ -81,6 +80,17 @@ class ThemeManager { void initializeIcons(); void initializeWidgets(); - const QStringList builtinIcons{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", - "OSX", "iOS", "flat", "flat_white", "multimc" }; + // On non-Mac systems, this is a no-op. + void setTitlebarColorOnMac(WId windowId, QColor color); + // This also will set the titlebar color of newly opened windows after this method is called. + // On non-Mac systems, this is a no-op. + void setTitlebarColorOfAllWindowsOnMac(QColor color); + // On non-Mac systems, this is a no-op. + void stopSettingNewWindowColorsOnMac(); +#ifdef Q_OS_MACOS + NSObject* m_windowTitlebarObserver = nullptr; +#endif + + const QStringList builtinIcons{"pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", + "OSX", "iOS", "flat", "flat_white", "multimc"}; }; diff --git a/launcher/ui/themes/ThemeManager.mm b/launcher/ui/themes/ThemeManager.mm new file mode 100644 index 000000000..d9fc291b6 --- /dev/null +++ b/launcher/ui/themes/ThemeManager.mm @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Kenneth Chew <79120643+kthchew@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ThemeManager.h" + +#include + +void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) +{ + if (windowId == 0) { + return; + } + + NSView* view = (NSView*)windowId; + NSWindow* window = [view window]; + window.titlebarAppearsTransparent = YES; + window.backgroundColor = [NSColor colorWithRed:color.redF() green:color.greenF() blue:color.blueF() alpha:color.alphaF()]; + + // Unfortunately there seems to be no easy way to set the titlebar text color. + // The closest we can do without dubious hacks is set the dark/light mode state based on the brightness of the + // background color, which should at least make the text readable even if we can't use the theme's text color. + // It's a good idea to set this anyway since it also affects some other UI elements like text shadows (PrismLauncher#3825). + if (color.lightnessF() < 0.5) { + window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + } else { + window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + } +} + +void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) +{ + NSArray* windows = [NSApp windows]; + for (NSWindow* window : windows) { + setTitlebarColorOnMac((WId)window.contentView, color); + } + + // We want to change the titlebar color of newly opened windows as well. + // There's no notification for when a new window is opened, but we can set the color when a window switches + // from occluded to visible, which also fires on open. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + stopSettingNewWindowColorsOnMac(); + m_windowTitlebarObserver = [center addObserverForName:NSWindowDidChangeOcclusionStateNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* notification) { + NSWindow* window = notification.object; + setTitlebarColorOnMac((WId)window.contentView, color); + }]; +} + +void ThemeManager::stopSettingNewWindowColorsOnMac() +{ + if (m_windowTitlebarObserver) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:m_windowTitlebarObserver]; + m_windowTitlebarObserver = nil; + } +} diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp new file mode 100644 index 000000000..0eedd1fc3 --- /dev/null +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 TheKodeToad + * Copyright (C) 2022 Tayou + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AppearanceWidget.h" +#include "ui_AppearanceWidget.h" + +#include +#include +#include "BuildConfig.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" + +AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) + : QWidget(parent), m_ui(new Ui::AppearanceWidget), m_themesOnly(themesOnly) +{ + m_ui->setupUi(this); + + m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this)); + + m_defaultFormat = QTextCharFormat(m_ui->consolePreview->currentCharFormat()); + + if (themesOnly) { + m_ui->catPackLabel->hide(); + m_ui->catPackComboBox->hide(); + m_ui->catPackFolder->hide(); + m_ui->settingsBox->hide(); + m_ui->consolePreview->hide(); + m_ui->catPreview->hide(); + loadThemeSettings(); + } else { + loadSettings(); + loadThemeSettings(); + + updateConsolePreview(); + updateCatPreview(); + } + + connect(m_ui->fontSizeBox, &QSpinBox::valueChanged, this, &AppearanceWidget::updateConsolePreview); + connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearanceWidget::updateConsolePreview); + + connect(m_ui->iconsComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyIconTheme); + connect(m_ui->widgetStyleComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyWidgetTheme); + connect(m_ui->catPackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyCatTheme); + connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview); + + connect(m_ui->iconsFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); + connect(m_ui->widgetStyleFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); + connect(m_ui->catPackFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearanceWidget::loadThemeSettings); +} + +AppearanceWidget::~AppearanceWidget() +{ + delete m_ui; +} + +void AppearanceWidget::applySettings() +{ + SettingsObject* settings = APPLICATION->settings(); + QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); + settings->set("ConsoleFont", consoleFontFamily); + settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); + settings->set("CatOpacity", m_ui->catOpacitySlider->value()); + auto catFit = m_ui->catFitComboBox->currentIndex(); + settings->set("CatFit", catFit == 0 ? "fit" : catFit == 1 ? "fill" : "strech"); +} + +void AppearanceWidget::loadSettings() +{ + SettingsObject* settings = APPLICATION->settings(); + QString fontFamily = settings->get("ConsoleFont").toString(); + QFont consoleFont(fontFamily); + m_ui->consoleFont->setCurrentFont(consoleFont); + + bool conversionOk = true; + int fontSize = settings->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_ui->fontSizeBox->setValue(fontSize); + + m_ui->catOpacitySlider->setValue(settings->get("CatOpacity").toInt()); + + auto catFit = settings->get("CatFit").toString(); + m_ui->catFitComboBox->setCurrentIndex(catFit == "fit" ? 0 : catFit == "fill" ? 1 : 2); +} + +void AppearanceWidget::retranslateUi() +{ + m_ui->retranslateUi(this); +} + +void AppearanceWidget::applyIconTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalIconTheme = settings->get("IconTheme").toString(); + auto newIconTheme = m_ui->iconsComboBox->itemData(index).toString(); + if (originalIconTheme != newIconTheme) { + settings->set("IconTheme", newIconTheme); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); + } +} + +void AppearanceWidget::applyWidgetTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalAppTheme = settings->get("ApplicationTheme").toString(); + auto newAppTheme = m_ui->widgetStyleComboBox->itemData(index).toString(); + if (originalAppTheme != newAppTheme) { + settings->set("ApplicationTheme", newAppTheme); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); + } + + updateConsolePreview(); +} + +void AppearanceWidget::applyCatTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalCat = settings->get("BackgroundCat").toString(); + auto newCat = m_ui->catPackComboBox->itemData(index).toString(); + if (originalCat != newCat) { + settings->set("BackgroundCat", newCat); + } + + APPLICATION->currentCatChanged(index); + updateCatPreview(); +} + +void AppearanceWidget::loadThemeSettings() +{ + APPLICATION->themeManager()->refresh(); + + m_ui->iconsComboBox->blockSignals(true); + m_ui->widgetStyleComboBox->blockSignals(true); + m_ui->catPackComboBox->blockSignals(true); + + m_ui->iconsComboBox->clear(); + m_ui->widgetStyleComboBox->clear(); + m_ui->catPackComboBox->clear(); + + SettingsObject* settings = APPLICATION->settings(); + + const QString currentIconTheme = settings->get("IconTheme").toString(); + const auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); + + for (int i = 0; i < iconThemes.count(); ++i) { + const IconTheme* theme = iconThemes[i]; + + QIcon iconForComboBox = QIcon(theme->path() + "/scalable/settings"); + m_ui->iconsComboBox->addItem(iconForComboBox, theme->name(), theme->id()); + + if (currentIconTheme == theme->id()) + m_ui->iconsComboBox->setCurrentIndex(i); + } + + const QString currentTheme = settings->get("ApplicationTheme").toString(); + auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); + for (int i = 0; i < themes.count(); ++i) { + ITheme* theme = themes[i]; + + m_ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + + if (!theme->tooltip().isEmpty()) + m_ui->widgetStyleComboBox->setItemData(i, theme->tooltip(), Qt::ToolTipRole); + + if (currentTheme == theme->id()) + m_ui->widgetStyleComboBox->setCurrentIndex(i); + } + + if (!m_themesOnly) { + const QString currentCat = settings->get("BackgroundCat").toString(); + const auto cats = APPLICATION->themeManager()->getValidCatPacks(); + for (int i = 0; i < cats.count(); ++i) { + const CatPack* cat = cats[i]; + + QIcon catIcon = QIcon(QString("%1").arg(cat->path())); + m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); + + if (currentCat == cat->id()) + m_ui->catPackComboBox->setCurrentIndex(i); + } + } + + m_ui->iconsComboBox->blockSignals(false); + m_ui->widgetStyleComboBox->blockSignals(false); + m_ui->catPackComboBox->blockSignals(false); +} + +void AppearanceWidget::updateConsolePreview() +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + int fontSize = m_ui->fontSizeBox->value(); + QString fontFamily = m_ui->consoleFont->currentFont().family(); + m_ui->consolePreview->clear(); + m_defaultFormat.setFont(QFont(fontFamily, fontSize)); + + auto print = [this, colors](const QString& message, MessageLevel level) { + QTextCharFormat format(m_defaultFormat); + + QColor bg = colors.background.value(level); + QColor fg = colors.foreground.value(level); + + if (bg.isValid()) + format.setBackground(bg); + + if (fg.isValid()) + format.setForeground(fg); + + // append a paragraph/line + auto workCursor = m_ui->consolePreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(message, format); + workCursor.insertBlock(); + }; + + print(QString("%1 version: %2\n").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()), MessageLevel::Launcher); + + QDate today = QDate::currentDate(); + + if (today.month() == 10 && today.day() == 31) + print(tr("[ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); + else + print(tr("[ERROR] A spooky error!"), MessageLevel::Error); + + print(tr("[INFO] A harmless message..."), MessageLevel::Info); + print(tr("[WARN] A not so spooky warning."), MessageLevel::Warning); + print(tr("[DEBUG] A secret debugging message..."), MessageLevel::Debug); + print(tr("[FATAL] A terrifying fatal error!"), MessageLevel::Fatal); +} + +void AppearanceWidget::updateCatPreview() +{ + QIcon catPackIcon(APPLICATION->themeManager()->getCatPack()); + m_ui->catPreview->setIcon(catPackIcon); + + auto effect = dynamic_cast(m_ui->catPreview->graphicsEffect()); + if (effect) + effect->setOpacity(m_ui->catOpacitySlider->value() / 100.0); +} diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/AppearanceWidget.h similarity index 58% rename from launcher/ui/widgets/ThemeCustomizationWidget.h rename to launcher/ui/widgets/AppearanceWidget.h index 6977b8495..1fc89af3a 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/AppearanceWidget.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify @@ -15,42 +16,47 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #pragma once -#include -#include "translations/TranslationsModel.h" +#include +#include + +#include +#include +#include +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" -enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; +class QTextCharFormat; +class SettingsObject; namespace Ui { -class ThemeCustomizationWidget; +class AppearanceWidget; } -class ThemeCustomizationWidget : public QWidget { +class AppearanceWidget : public QWidget { Q_OBJECT public: - explicit ThemeCustomizationWidget(QWidget* parent = nullptr); - ~ThemeCustomizationWidget() override; - - void showFeatures(ThemeFields features); + explicit AppearanceWidget(bool simple, QWidget* parent = 0); + virtual ~AppearanceWidget(); + public: void applySettings(); - void loadSettings(); - void retranslate(); + void retranslateUi(); - private slots: + private: void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); - void refresh(); + void loadThemeSettings(); - signals: - int currentIconThemeChanged(int index); - int currentWidgetThemeChanged(int index); - int currentCatChanged(int index); + void updateConsolePreview(); + void updateCatPreview(); - private: - Ui::ThemeCustomizationWidget* ui; + Ui::AppearanceWidget* m_ui; + QTextCharFormat m_defaultFormat; + bool m_themesOnly; }; diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui new file mode 100644 index 000000000..f74ab64b5 --- /dev/null +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -0,0 +1,632 @@ + + + AppearanceWidget + + + + 0 + 0 + 600 + 583 + + + + + 300 + 0 + + + + + + + + + + false + + + + + + + + View cat packs folder. + + + Open Folder + + + + + + + View widget themes folder. + + + Open Folder + + + + + + + View icon themes folder. + + + Open Folder + + + + + + + &Cat Pack: + + + catPackComboBox + + + + + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::StrongFocus + + + + + + + + 0 + 0 + + + + Reload All + + + + + + + Theme: + + + widgetStyleComboBox + + + + + + + &Icons: + + + iconsComboBox + + + + + + + + + + + + + + + + + + + + Console Font: + + + + + + + + + + + 0 + 0 + + + + 5 + + + 16 + + + 11 + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 0 + 6 + + + + + + + + Cat Opacity + + + + + + + + 300 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 300 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + Opaque + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + false + + + Transparent + + + + + + + + 0 + 0 + + + + 100 + + + Qt::Orientation::Horizontal + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 0 + 6 + + + + + + + + + 0 + 0 + + + + Cat Scaling + + + + + + + + 0 + 0 + + + + + 81 + 32 + + + + 0 + + + + Fit + + + + + Fill + + + + + Stretch + + + + + + + + + + + Preview + + + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::NoFocus + + + + + + + 64 + 128 + + + + true + + + + + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::FocusPolicy::NoFocus + + + + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Qt::ScrollBarPolicy::ScrollBarAsNeeded + + + false + + + Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse + + + + + + + + + + + + + + widgetStyleComboBox + widgetStyleFolder + iconsComboBox + iconsFolder + catPackComboBox + catPackFolder + reloadThemesButton + consoleFont + fontSizeBox + catFitComboBox + catOpacitySlider + consolePreview + + + + diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp index 02b629162..dddf5333e 100644 --- a/launcher/ui/widgets/CheckComboBox.cpp +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -84,7 +84,7 @@ void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) proxy->setSourceModel(new_model); model()->disconnect(this); QComboBox::setModel(proxy); - connect(this, QOverload::of(&QComboBox::activated), this, &CheckComboBox::toggleCheckState); + connect(this, &QComboBox::activated, this, &CheckComboBox::toggleCheckState); connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged); @@ -138,7 +138,7 @@ bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event) } case QEvent::MouseButtonPress: { auto ev = static_cast(event); - m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid(); + m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid() && view()->rect().contains(ev->pos()); break; } case QEvent::Wheel: @@ -178,7 +178,7 @@ QStringList CheckComboBox::checkedItems() const void CheckComboBox::setCheckedItems(const QStringList& items) { - foreach (auto text, items) { + for (auto text : items) { auto index = findText(text); setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); } @@ -203,4 +203,4 @@ void CheckComboBox::paintEvent(QPaintEvent*) painter.drawControl(QStyle::CE_ComboBoxLabel, opt); } -#include "CheckComboBox.moc" \ No newline at end of file +#include "CheckComboBox.moc" diff --git a/launcher/ui/widgets/CheckComboBox.h b/launcher/ui/widgets/CheckComboBox.h index 469587762..10e2a81be 100644 --- a/launcher/ui/widgets/CheckComboBox.h +++ b/launcher/ui/widgets/CheckComboBox.h @@ -61,4 +61,4 @@ class CheckComboBox : public QComboBox { QString m_default_text; QString m_separator; bool m_containerMousePress = false; -}; \ No newline at end of file +}; diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp index 9b98d7409..ddeaefc4b 100644 --- a/launcher/ui/widgets/CustomCommands.cpp +++ b/launcher/ui/widgets/CustomCommands.cpp @@ -44,13 +44,14 @@ CustomCommands::~CustomCommands() CustomCommands::CustomCommands(QWidget* parent) : QWidget(parent), ui(new Ui::CustomCommands) { ui->setupUi(this); + connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->customCommandsWidget, &QWidget::setEnabled); } void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit) { - ui->customCommandsGroupBox->setCheckable(checkable); + ui->overrideCheckBox->setVisible(checkable); if (checkable) { - ui->customCommandsGroupBox->setChecked(checked); + ui->overrideCheckBox->setChecked(checked); } ui->preLaunchCmdTextBox->setText(prelaunch); ui->wrapperCmdTextBox->setText(wrapper); @@ -64,9 +65,7 @@ void CustomCommands::retranslate() bool CustomCommands::checked() const { - if (!ui->customCommandsGroupBox->isCheckable()) - return true; - return ui->customCommandsGroupBox->isChecked(); + return ui->overrideCheckBox->isChecked(); } QString CustomCommands::prelaunchCommand() const diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index b485c293e..6c1366c06 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -24,58 +24,100 @@ 0
    - - - true - - - &Custom Commands + + + Override &Global Settings - + true - - false + + + + + + true + + 0 + + + 0 + + + 0 + - &Pre-launch command: + &Pre-launch Command preLaunchCmdTextBox - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + - + + + + + + + - &Wrapper command: + P&ost-exit Command - wrapperCmdTextBox + postExitCmdTextBox - - + + - - + + - P&ost-exit command: + &Wrapper Command - postExitCmdTextBox + wrapperCmdTextBox - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + diff --git a/launcher/ui/widgets/DropLabel.cpp b/launcher/ui/widgets/DropLabel.cpp deleted file mode 100644 index b1473b358..000000000 --- a/launcher/ui/widgets/DropLabel.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "DropLabel.h" - -#include -#include - -DropLabel::DropLabel(QWidget* parent) : QLabel(parent) -{ - setAcceptDrops(true); -} - -void DropLabel::dragEnterEvent(QDragEnterEvent* event) -{ - event->acceptProposedAction(); -} - -void DropLabel::dragMoveEvent(QDragMoveEvent* event) -{ - event->acceptProposedAction(); -} - -void DropLabel::dragLeaveEvent(QDragLeaveEvent* event) -{ - event->accept(); -} - -void DropLabel::dropEvent(QDropEvent* event) -{ - const QMimeData* mimeData = event->mimeData(); - - if (!mimeData) { - return; - } - - if (mimeData->hasUrls()) { - auto urls = mimeData->urls(); - emit droppedURLs(urls); - } - - event->acceptProposedAction(); -} diff --git a/launcher/ui/widgets/DropLabel.h b/launcher/ui/widgets/DropLabel.h deleted file mode 100644 index 0027f48b1..000000000 --- a/launcher/ui/widgets/DropLabel.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -class DropLabel : public QLabel { - Q_OBJECT - - public: - explicit DropLabel(QWidget* parent = nullptr); - - signals: - void droppedURLs(QList urls); - - protected: - void dropEvent(QDropEvent* event) override; - void dragEnterEvent(QDragEnterEvent* event) override; - void dragMoveEvent(QDragMoveEvent* event) override; - void dragLeaveEvent(QDragLeaveEvent* event) override; -}; diff --git a/launcher/ui/widgets/EnvironmentVariables.cpp b/launcher/ui/widgets/EnvironmentVariables.cpp index 633fc6122..9387ef2e2 100644 --- a/launcher/ui/widgets/EnvironmentVariables.cpp +++ b/launcher/ui/widgets/EnvironmentVariables.cpp @@ -50,6 +50,8 @@ EnvironmentVariables::EnvironmentVariables(QWidget* parent) : QWidget(parent), u }); connect(ui->clear, &QPushButton::clicked, this, [this] { ui->list->clear(); }); + + connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->settingsWidget, &QWidget::setEnabled); } EnvironmentVariables::~EnvironmentVariables() @@ -60,8 +62,8 @@ EnvironmentVariables::~EnvironmentVariables() void EnvironmentVariables::initialize(bool instance, bool override, const QMap& value) { // update widgets to settings - ui->groupBox->setCheckable(instance); - ui->groupBox->setChecked(override); + ui->overrideCheckBox->setVisible(instance); + ui->overrideCheckBox->setChecked(override); // populate ui->list->clear(); @@ -94,9 +96,7 @@ void EnvironmentVariables::retranslate() bool EnvironmentVariables::override() const { - if (!ui->groupBox->isCheckable()) - return false; - return ui->groupBox->isChecked(); + return ui->overrideCheckBox->isChecked(); } QMap EnvironmentVariables::value() const diff --git a/launcher/ui/widgets/EnvironmentVariables.ui b/launcher/ui/widgets/EnvironmentVariables.ui index ded5b2ded..cc52b5d10 100644 --- a/launcher/ui/widgets/EnvironmentVariables.ui +++ b/launcher/ui/widgets/EnvironmentVariables.ui @@ -14,27 +14,72 @@ Form
    - - 0 - - - 0 - - - 0 - - - 0 - - - - &Environment Variables + + + Override &Global Settings - + true - + + + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + &Add + + + + + + + &Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Clear + + + + + @@ -67,44 +112,6 @@ - - - - - - &Add - - - - - - - &Remove - - - - - - - &Clear - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - diff --git a/launcher/ui/widgets/ErrorFrame.cpp b/launcher/ui/widgets/ErrorFrame.cpp deleted file mode 100644 index 213c26b76..000000000 --- a/launcher/ui/widgets/ErrorFrame.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include "ErrorFrame.h" -#include "ui_ErrorFrame.h" - -#include "ui/dialogs/CustomMessageBox.h" - -void ErrorFrame::clear() -{ - setTitle(QString()); - setDescription(QString()); -} - -ErrorFrame::ErrorFrame(QWidget* parent) : QFrame(parent), ui(new Ui::ErrorFrame) -{ - ui->setupUi(this); - ui->label_Description->setHidden(true); - ui->label_Title->setHidden(true); - updateHiddenState(); -} - -ErrorFrame::~ErrorFrame() -{ - delete ui; -} - -void ErrorFrame::updateHiddenState() -{ - if (ui->label_Description->isHidden() && ui->label_Title->isHidden()) { - setHidden(true); - } else { - setHidden(false); - } -} - -void ErrorFrame::setTitle(QString text) -{ - if (text.isEmpty()) { - ui->label_Title->setHidden(true); - } else { - ui->label_Title->setText(text); - ui->label_Title->setHidden(false); - } - updateHiddenState(); -} - -void ErrorFrame::setDescription(QString text) -{ - if (text.isEmpty()) { - ui->label_Description->setHidden(true); - updateHiddenState(); - return; - } else { - ui->label_Description->setHidden(false); - updateHiddenState(); - } - ui->label_Description->setToolTip(""); - QString intermediatetext = text.trimmed(); - bool prev(false); - QChar rem('\n'); - QString finaltext; - finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { - if (c == rem && prev) { - continue; - } - prev = c == rem; - finaltext += c; - } - QString labeltext; - labeltext.reserve(300); - if (finaltext.length() > 290) { - ui->label_Description->setOpenExternalLinks(false); - ui->label_Description->setTextFormat(Qt::TextFormat::RichText); - desc = text; - // This allows injecting HTML here. - labeltext.append("" + finaltext.left(287) + "..."); - QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler); - } else { - ui->label_Description->setTextFormat(Qt::TextFormat::PlainText); - labeltext.append(finaltext); - } - ui->label_Description->setText(labeltext); -} - -void ErrorFrame::ellipsisHandler(const QString& link) -{ - if (!currentBox) { - currentBox = CustomMessageBox::selectable(this, QString(), desc); - connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed); - currentBox->show(); - } else { - currentBox->setText(desc); - } -} - -void ErrorFrame::boxClosed(int result) -{ - currentBox = nullptr; -} diff --git a/launcher/ui/widgets/ErrorFrame.h b/launcher/ui/widgets/ErrorFrame.h deleted file mode 100644 index 1aea6a1d8..000000000 --- a/launcher/ui/widgets/ErrorFrame.h +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -namespace Ui { -class ErrorFrame; -} - -class ErrorFrame : public QFrame { - Q_OBJECT - - public: - explicit ErrorFrame(QWidget* parent = 0); - ~ErrorFrame(); - - void setTitle(QString text); - void setDescription(QString text); - - void clear(); - - public slots: - void ellipsisHandler(const QString& link); - void boxClosed(int result); - - private: - void updateHiddenState(); - - private: - Ui::ErrorFrame* ui; - QString desc; - class QMessageBox* currentBox = nullptr; -}; diff --git a/launcher/ui/widgets/ErrorFrame.ui b/launcher/ui/widgets/ErrorFrame.ui deleted file mode 100644 index 0bb567439..000000000 --- a/launcher/ui/widgets/ErrorFrame.ui +++ /dev/null @@ -1,92 +0,0 @@ - - - ErrorFrame - - - - 0 - 0 - 527 - 113 - - - - - 0 - 0 - - - - - 16777215 - 120 - - - - - 6 - - - 0 - - - 0 - - - 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 - - - - - - - - diff --git a/launcher/ui/widgets/FocusLineEdit.cpp b/launcher/ui/widgets/FocusLineEdit.cpp deleted file mode 100644 index 6570227bb..000000000 --- a/launcher/ui/widgets/FocusLineEdit.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "FocusLineEdit.h" -#include - -FocusLineEdit::FocusLineEdit(QWidget* parent) : QLineEdit(parent) -{ - _selectOnMousePress = false; -} - -void FocusLineEdit::focusInEvent(QFocusEvent* e) -{ - QLineEdit::focusInEvent(e); - selectAll(); - _selectOnMousePress = true; -} - -void FocusLineEdit::mousePressEvent(QMouseEvent* me) -{ - QLineEdit::mousePressEvent(me); - if (_selectOnMousePress) { - selectAll(); - _selectOnMousePress = false; - } - qDebug() << selectedText(); -} diff --git a/launcher/ui/widgets/FocusLineEdit.h b/launcher/ui/widgets/FocusLineEdit.h deleted file mode 100644 index f5ea6602e..000000000 --- a/launcher/ui/widgets/FocusLineEdit.h +++ /dev/null @@ -1,14 +0,0 @@ -#include - -class FocusLineEdit : public QLineEdit { - Q_OBJECT - public: - FocusLineEdit(QWidget* parent); - virtual ~FocusLineEdit() {} - - protected: - void focusInEvent(QFocusEvent* e); - void mousePressEvent(QMouseEvent* me); - - bool _selectOnMousePress; -}; diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 44f702659..1e641c4f9 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -84,7 +84,7 @@ void InfoFrame::updateWithMod(Mod const& m) QString text = ""; QString name = ""; - QString link = m.metaurl(); + QString link = m.homepage(); if (m.name().isEmpty()) name = m.internal_id(); else @@ -93,7 +93,7 @@ void InfoFrame::updateWithMod(Mod const& m) if (link.isEmpty()) text = name; else { - text = "" + name + ""; + text = "" + name + ""; } if (!m.authors().isEmpty()) text += " by " + m.authors().join(", "); @@ -145,7 +145,13 @@ void InfoFrame::updateWithMod(Mod const& m) void InfoFrame::updateWithResource(const Resource& resource) { - setName(resource.name()); + const QString homepage = resource.homepage(); + + if (!homepage.isEmpty()) + setName("" + resource.name() + ""); + else + setName(resource.name()); + setImage(); } @@ -209,14 +215,35 @@ QString InfoFrame::renderColorCodes(QString input) void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) { - setName(renderColorCodes(resource_pack.name())); + QString name = renderColorCodes(resource_pack.name()); + + const QString homepage = resource_pack.homepage(); + if (!homepage.isEmpty()) { + name = "" + name + ""; + } + + setName(name); setDescription(renderColorCodes(resource_pack.description())); setImage(resource_pack.image({ 64, 64 })); } +void InfoFrame::updateWithDataPack(DataPack& data_pack) +{ + setName(renderColorCodes(data_pack.name())); + setDescription(renderColorCodes(data_pack.description())); + setImage(data_pack.image({ 64, 64 })); +} + void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { - setName(renderColorCodes(texture_pack.name())); + QString name = renderColorCodes(texture_pack.name()); + + const QString homepage = texture_pack.homepage(); + if (!homepage.isEmpty()) { + name = "" + name + ""; + } + + setName(name); setDescription(renderColorCodes(texture_pack.description())); setImage(texture_pack.image({ 64, 64 })); } @@ -267,7 +294,7 @@ void InfoFrame::setDescription(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } @@ -297,7 +324,7 @@ void InfoFrame::setDescription(QString text) cursor.insertHtml("..."); labeltext.append(doc.toHtml()); - QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); @@ -321,7 +348,7 @@ void InfoFrame::setLicense(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } @@ -336,7 +363,7 @@ void InfoFrame::setLicense(QString text) m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); - QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index d6764baa2..20c54e2e5 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -37,6 +37,7 @@ #include +#include "minecraft/mod/DataPack.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/TexturePack.h" @@ -63,6 +64,7 @@ class InfoFrame : public QFrame { void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); + void updateWithDataPack(DataPack& rp); void updateWithTexturePack(TexturePack& tp); static QString renderColorCodes(QString input); diff --git a/launcher/ui/widgets/InstanceCardWidget.ui b/launcher/ui/widgets/InstanceCardWidget.ui deleted file mode 100644 index 6eeeb0769..000000000 --- a/launcher/ui/widgets/InstanceCardWidget.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - InstanceCardWidget - - - - 0 - 0 - 473 - 118 - - - - - - - - 80 - 80 - - - - - - - - &Name: - - - instNameTextBox - - - - - - - - - - &Group: - - - groupBox - - - - - - - true - - - - - - - - diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 6efd3f581..1763bd953 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -1,559 +1,307 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "JavaSettingsWidget.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "DesktopServices.h" +#include +#include "Application.h" +#include "BuildConfig.h" #include "FileSystem.h" #include "JavaCommon.h" -#include "java/JavaChecker.h" -#include "java/JavaInstall.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/widgets/VersionSelectWidget.h" -#include "Application.h" -#include "BuildConfig.h" +#include "ui_JavaSettingsWidget.h" -JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) +JavaSettingsWidget::JavaSettingsWidget(BaseInstance* instance, QWidget* parent) + : QWidget(parent), m_instance(instance), m_ui(new Ui::JavaSettingsWidget) { - m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; - - goodIcon = APPLICATION->getThemedIcon("status-good"); - yellowIcon = APPLICATION->getThemedIcon("status-yellow"); - badIcon = APPLICATION->getThemedIcon("status-bad"); - m_memoryTimer = new QTimer(this); - setupUi(); - - connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_memoryTimer, &QTimer::timeout, this, &JavaSettingsWidget::memoryValueChanged); - connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected); - connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked); - connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited); - connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaSettingsWidget::javaDownloadBtn_clicked); - } -} + m_ui->setupUi(this); + + if (m_instance == nullptr) { + m_ui->javaDownloadBtn->hide(); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this](bool state) { + m_ui->autodownloadJavaCheckBox->setEnabled(state); + if (!state) + m_ui->autodownloadJavaCheckBox->setChecked(false); + }); + } else { + m_ui->autodownloadJavaCheckBox->hide(); + } + } else { + m_ui->javaDownloadBtn->setVisible(BuildConfig.JAVA_DOWNLOADER_ENABLED); + m_ui->skipWizardCheckBox->hide(); + m_ui->autodetectJavaCheckBox->hide(); + m_ui->autodownloadJavaCheckBox->hide(); -void JavaSettingsWidget::setupUi() -{ - setObjectName(QStringLiteral("javaSettingsWidget")); - m_verticalLayout = new QVBoxLayout(this); - m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - - m_versionWidget = new VersionSelectWidget(this); - - m_horizontalLayout = new QHBoxLayout(); - m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); - m_javaPathTextBox = new QLineEdit(this); - m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); - - m_horizontalLayout->addWidget(m_javaPathTextBox); - - m_javaBrowseBtn = new QPushButton(this); - m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); - - m_horizontalLayout->addWidget(m_javaBrowseBtn); - - m_javaStatusBtn = new QToolButton(this); - m_javaStatusBtn->setIcon(yellowIcon); - m_horizontalLayout->addWidget(m_javaStatusBtn); - - m_memoryGroupBox = new QGroupBox(this); - m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); - m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); - m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); - m_gridLayout_2->setColumnStretch(0, 1); - - m_labelMinMem = new QLabel(m_memoryGroupBox); - m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); - m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); - - m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); - m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); - m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); - m_minMemSpinBox->setMinimum(8); - m_minMemSpinBox->setMaximum(1048576); - m_minMemSpinBox->setSingleStep(128); - m_labelMinMem->setBuddy(m_minMemSpinBox); - m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); - - m_labelMaxMem = new QLabel(m_memoryGroupBox); - m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); - m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); - - m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); - m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); - m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); - m_maxMemSpinBox->setMinimum(8); - m_maxMemSpinBox->setMaximum(1048576); - m_maxMemSpinBox->setSingleStep(128); - m_labelMaxMem->setBuddy(m_maxMemSpinBox); - m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); - - m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); - m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); - m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); - - m_labelPermGen = new QLabel(m_memoryGroupBox); - m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); - m_labelPermGen->setText(QStringLiteral("PermGen:")); - m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); - m_labelPermGen->setVisible(false); - - m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); - m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); - m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); - m_permGenSpinBox->setMinimum(4); - m_permGenSpinBox->setMaximum(1048576); - m_permGenSpinBox->setSingleStep(8); - m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); - m_permGenSpinBox->setVisible(false); - - m_verticalLayout->addWidget(m_memoryGroupBox); - - m_horizontalBtnLayout = new QHBoxLayout(); - m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout")); - - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_javaDownloadBtn = new QPushButton(tr("Download Java"), this); - m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); - } + m_ui->javaInstallationGroupBox->setCheckable(true); + m_ui->memoryGroupBox->setCheckable(true); + m_ui->javaArgumentsGroupBox->setCheckable(true); - m_autoJavaGroupBox = new QGroupBox(this); - m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); - m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); - m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout")); - - m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox); - m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox"); - m_autodetectJavaCheckBox->setChecked(true); - m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox); - - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox); - m_autodownloadCheckBox->setObjectName("autodownloadCheckBox"); - m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); - m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox); - connect(m_autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { - m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); - if (!m_autodetectJavaCheckBox->isChecked()) - m_autodownloadCheckBox->setChecked(false); - }); + 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()); }); + connect(settings->getSetting("JavaPath").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, + [this, settings] { m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); }); - connect(m_autodownloadCheckBox, &QCheckBox::stateChanged, this, [this] { - auto isChecked = m_autodownloadCheckBox->isChecked(); - m_versionWidget->setVisible(!isChecked); - m_javaStatusBtn->setVisible(!isChecked); - m_javaBrowseBtn->setVisible(!isChecked); - m_javaPathTextBox->setVisible(!isChecked); - m_javaDownloadBtn->setVisible(!isChecked); - if (!isChecked) { - m_verticalLayout->removeItem(m_verticalSpacer); - } else { - m_verticalLayout->addSpacerItem(m_verticalSpacer); + connect(m_ui->javaDownloadBtn, &QPushButton::clicked, this, [this] { + auto javaDialog = new Java::InstallDialog({}, m_instance, this); + javaDialog->exec(); + }); + connect(m_ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { + if (m_instance->settings()->get("JavaPath").toString() != newValue) { + m_instance->settings()->set("AutomaticJava", false); } }); } - m_verticalLayout->addWidget(m_autoJavaGroupBox); - m_verticalLayout->addLayout(m_horizontalBtnLayout); + connect(m_ui->javaTestBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaTest); + connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); + connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); - m_verticalLayout->addWidget(m_versionWidget); - m_verticalLayout->addLayout(m_horizontalLayout); - m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + connect(m_ui->maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); - retranslate(); + loadSettings(); + updateThresholds(); } -void JavaSettingsWidget::initialize() +JavaSettingsWidget::~JavaSettingsWidget() { - m_versionWidget->initialize(APPLICATION->javalist().get()); - m_versionWidget->selectSearch(); - m_versionWidget->setResizeOn(2); - auto s = APPLICATION->settings(); - // Memory - observedMinMemory = s->get("MinMemAlloc").toInt(); - observedMaxMemory = s->get("MaxMemAlloc").toInt(); - observedPermGenMemory = s->get("PermGen").toInt(); - m_minMemSpinBox->setValue(observedMinMemory); - m_maxMemSpinBox->setValue(observedMaxMemory); - m_permGenSpinBox->setValue(observedPermGenMemory); - updateThresholds(); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_autodownloadCheckBox->setChecked(true); - } + delete m_ui; } -void JavaSettingsWidget::refresh() +void JavaSettingsWidget::loadSettings() { - if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { - return; - } - if (JavaUtils::getJavaCheckPath().isEmpty()) { - JavaCommon::javaCheckNotFound(this); - return; + SettingsObject* settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + // Java Settings + m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); + m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); + + m_ui->skipCompatibilityCheckBox->setChecked(settings->get("IgnoreJavaCompatibility").toBool()); + + m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); + m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); + + if (m_instance == nullptr) { + m_ui->skipWizardCheckBox->setChecked(settings->get("IgnoreJavaWizard").toBool()); + m_ui->autodetectJavaCheckBox->setChecked(settings->get("AutomaticJavaSwitch").toBool()); + m_ui->autodetectJavaCheckBox->stateChanged(m_ui->autodetectJavaCheckBox->isChecked()); + m_ui->autodownloadJavaCheckBox->setChecked(settings->get("AutomaticJavaDownload").toBool()); } - m_versionWidget->loadList(); -} -JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate() -{ - switch (javaStatus) { - default: - case JavaStatus::NotSet: - /* fallthrough */ - case JavaStatus::DoesNotExist: - /* fallthrough */ - case JavaStatus::DoesNotStart: - /* fallthrough */ - case JavaStatus::ReturnedInvalidData: { - if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())) { // the java will not be autodownloaded - int button = QMessageBox::No; - if (m_result.mojangPlatform == "32" && maxHeapSize() > 2048) { - button = CustomMessageBox::selectable( - this, tr("32-bit Java detected"), - tr("You selected a 32-bit installation of Java, but allocated more than 2048MiB as maximum memory.\n" - "%1 will not be able to start Minecraft.\n" - "Do you wish to proceed?" - "\n\n" - "You can change the Java version in the settings later.\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) - ->exec(); - - } else { - button = CustomMessageBox::selectable(this, tr("No Java version selected"), - tr("You either didn't select a Java version or selected one that does not work.\n" - "%1 will not be able to start Minecraft.\n" - "Do you wish to proceed without a functional version of Java?" - "\n\n" - "You can change the Java version in the settings later.\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, - QMessageBox::NoButton) - ->exec(); - } - switch (button) { - case QMessageBox::Yes: - return ValidationStatus::JavaBad; - case QMessageBox::Help: - DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); - /* fallthrough */ - case QMessageBox::No: - /* fallthrough */ - default: - return ValidationStatus::Bad; - } - if (button == QMessageBox::No) { - return ValidationStatus::Bad; - } - } - return ValidationStatus::JavaBad; - } break; - case JavaStatus::Pending: { - return ValidationStatus::Bad; - } - case JavaStatus::Good: { - return ValidationStatus::AllOK; - } + // Memory + m_ui->memoryGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideMemory").toBool()); + int min = settings->get("MinMemAlloc").toInt(); + int max = settings->get("MaxMemAlloc").toInt(); + if (min < max) { + m_ui->minMemSpinBox->setValue(min); + m_ui->maxMemSpinBox->setValue(max); + } else { + m_ui->minMemSpinBox->setValue(max); + m_ui->maxMemSpinBox->setValue(min); } -} + m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt()); -QString JavaSettingsWidget::javaPath() const -{ - return m_javaPathTextBox->text(); + // Java arguments + m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); + m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); } -int JavaSettingsWidget::maxHeapSize() const +void JavaSettingsWidget::saveSettings() { - auto min = m_minMemSpinBox->value(); - auto max = m_maxMemSpinBox->value(); - if (max < min) - max = min; - return max; -} + SettingsObject* settings; -int JavaSettingsWidget::minHeapSize() const -{ - auto min = m_minMemSpinBox->value(); - auto max = m_maxMemSpinBox->value(); - if (min > max) - min = max; - return min; -} + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); -bool JavaSettingsWidget::permGenEnabled() const -{ - return m_permGenSpinBox->isVisible(); -} + SettingsObject::Lock lock(settings); -int JavaSettingsWidget::permGenSize() const -{ - return m_permGenSpinBox->value(); -} + // Java Install Settings + bool javaInstall = m_instance == nullptr || m_ui->javaInstallationGroupBox->isChecked(); -void JavaSettingsWidget::memoryValueChanged() -{ - bool actuallyChanged = false; - unsigned int min = m_minMemSpinBox->value(); - unsigned int max = m_maxMemSpinBox->value(); - unsigned int permgen = m_permGenSpinBox->value(); - if (min != observedMinMemory) { - observedMinMemory = min; - actuallyChanged = true; - } - if (max != observedMaxMemory) { - observedMaxMemory = max; - actuallyChanged = true; - } - if (permgen != observedPermGenMemory) { - observedPermGenMemory = permgen; - actuallyChanged = true; - } - if (actuallyChanged) { - checkJavaPathOnEdit(m_javaPathTextBox->text()); - updateThresholds(); - } -} + if (m_instance != nullptr) + settings->set("OverrideJavaLocation", javaInstall); -void JavaSettingsWidget::javaVersionSelected(BaseVersion::Ptr version) -{ - auto java = std::dynamic_pointer_cast(version); - if (!java) { - return; + if (javaInstall) { + settings->set("JavaPath", m_ui->javaPathTextBox->text()); + settings->set("IgnoreJavaCompatibility", m_ui->skipCompatibilityCheckBox->isChecked()); + } else { + settings->reset("JavaPath"); + settings->reset("IgnoreJavaCompatibility"); } - auto visible = java->id.requiresPermGen(); - m_labelPermGen->setVisible(visible); - m_permGenSpinBox->setVisible(visible); - m_javaPathTextBox->setText(java->path); - checkJavaPath(java->path); -} -void JavaSettingsWidget::on_javaBrowseBtn_clicked() -{ - auto filter = QString("Java (%1)").arg(JavaUtils::javaExecutable); - auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); - if (raw_path.isEmpty()) { - return; + if (m_instance == nullptr) { + settings->set("IgnoreJavaWizard", m_ui->skipWizardCheckBox->isChecked()); + settings->set("AutomaticJavaSwitch", m_ui->autodetectJavaCheckBox->isChecked()); + settings->set("AutomaticJavaDownload", m_ui->autodownloadJavaCheckBox->isChecked()); } - auto cooked_path = FS::NormalizePath(raw_path); - m_javaPathTextBox->setText(cooked_path); - checkJavaPath(cooked_path); -} - -void JavaSettingsWidget::javaDownloadBtn_clicked() -{ - auto jdialog = new Java::InstallDialog({}, nullptr, this); - jdialog->exec(); -} -void JavaSettingsWidget::on_javaStatusBtn_clicked() -{ - QString text; - bool failed = false; - switch (javaStatus) { - case JavaStatus::NotSet: - checkJavaPath(m_javaPathTextBox->text()); - return; - case JavaStatus::DoesNotExist: - text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); - failed = true; - break; - case JavaStatus::DoesNotStart: { - text += QObject::tr("The specified Java binary didn't start properly.
    "); - auto htmlError = m_result.errorLog; - if (!htmlError.isEmpty()) { - htmlError.replace('\n', "
    "); - text += QString("%1").arg(htmlError); - } - failed = true; - break; - } - case JavaStatus::ReturnedInvalidData: { - text += QObject::tr("The specified Java binary returned unexpected results:
    "); - auto htmlOut = m_result.outLog; - if (!htmlOut.isEmpty()) { - htmlOut.replace('\n', "
    "); - text += QString("%1").arg(htmlOut); - } - failed = true; - break; + // Memory + bool memory = m_instance == nullptr || m_ui->memoryGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideMemory", memory); + + if (memory) { + int min = m_ui->minMemSpinBox->value(); + int max = m_ui->maxMemSpinBox->value(); + if (min < max) { + settings->set("MinMemAlloc", min); + settings->set("MaxMemAlloc", max); + } else { + settings->set("MinMemAlloc", max); + settings->set("MaxMemAlloc", min); } - case JavaStatus::Good: - text += QObject::tr( - "Java test succeeded!
    Platform reported: %1
    Java version " - "reported: %2
    ") - .arg(m_result.realPlatform, m_result.javaVersion.toString()); - break; - case JavaStatus::Pending: - // TODO: abort here? - return; + settings->set("PermGen", m_ui->permGenSpinBox->value()); + } else { + settings->reset("MinMemAlloc"); + settings->reset("MaxMemAlloc"); + settings->reset("PermGen"); } - CustomMessageBox::selectable(this, failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, - failed ? QMessageBox::Critical : QMessageBox::Information) - ->show(); -} -void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status) -{ - javaStatus = status; - switch (javaStatus) { - case JavaStatus::Good: - m_javaStatusBtn->setIcon(goodIcon); - break; - case JavaStatus::NotSet: - case JavaStatus::Pending: - m_javaStatusBtn->setIcon(yellowIcon); - break; - default: - m_javaStatusBtn->setIcon(badIcon); - break; - } -} + // Java arguments + bool javaArgs = m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked(); -void JavaSettingsWidget::javaPathEdited(const QString& path) -{ - checkJavaPathOnEdit(path); -} + if (m_instance != nullptr) + settings->set("OverrideJavaArgs", javaArgs); -void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path) -{ - auto realPath = FS::ResolveExecutable(path); - QFileInfo pathInfo(realPath); - if (pathInfo.baseName().toLower().contains("java")) { - checkJavaPath(path); + if (javaArgs) { + settings->set("JvmArgs", m_ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); } else { - if (!m_checker) { - setJavaStatus(JavaStatus::NotSet); - } + settings->reset("JvmArgs"); } } -void JavaSettingsWidget::checkJavaPath(const QString& path) +void JavaSettingsWidget::onJavaBrowse() { - if (m_checker) { - queuedCheck = path; + QString rawPath = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (rawPath.isEmpty()) { return; } - auto realPath = FS::ResolveExecutable(path); - if (realPath.isNull()) { - setJavaStatus(JavaStatus::DoesNotExist); + + QString cookedPath = FS::NormalizePath(rawPath); + QFileInfo javaInfo(cookedPath); + if (!javaInfo.exists() || !javaInfo.isExecutable()) { return; } - setJavaStatus(JavaStatus::Pending); - m_checker.reset( - new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0)); - connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); - m_checker->start(); + m_ui->javaPathTextBox->setText(cookedPath); } -void JavaSettingsWidget::checkFinished(const JavaChecker::Result& result) +void JavaSettingsWidget::onJavaTest() { - m_result = result; - switch (result.validity) { - case JavaChecker::Result::Validity::Valid: { - setJavaStatus(JavaStatus::Good); - break; - } - case JavaChecker::Result::Validity::ReturnedInvalidData: { - setJavaStatus(JavaStatus::ReturnedInvalidData); - break; - } - case JavaChecker::Result::Validity::Errored: { - setJavaStatus(JavaStatus::DoesNotStart); - break; - } - } - updateThresholds(); - m_checker.reset(); - if (!queuedCheck.isNull()) { - checkJavaPath(queuedCheck); - queuedCheck.clear(); - } -} + if (m_checker != nullptr) + return; -void JavaSettingsWidget::retranslate() -{ - m_memoryGroupBox->setTitle(tr("Memory")); - m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); - m_labelMinMem->setText(tr("Minimum memory allocation:")); - m_labelMaxMem->setText(tr("Maximum memory allocation:")); - m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); - m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); - m_javaBrowseBtn->setText(tr("Browse")); - if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { - m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); - } - m_autodetectJavaCheckBox->setText(tr("Autodetect Java version")); - m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); -} + QString jvmArgs; -void JavaSettingsWidget::updateThresholds() -{ - QString iconName; - - if (observedMaxMemory >= m_availableMemory) { - iconName = "status-bad"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); - } else if (observedMaxMemory > (m_availableMemory * 0.9)) { - iconName = "status-yellow"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (observedMaxMemory < observedMinMemory) { - iconName = "status-yellow"; - m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); - } else if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { - iconName = "status-good"; - m_labelMaxMemIcon->setToolTip(""); - } else if (observedMaxMemory > 2048 && !m_result.is_64bit) { - iconName = "status-bad"; - m_labelMaxMemIcon->setToolTip(tr("You are exceeding the maximum allocation supported by 32-bit installations of Java.")); - } else { - iconName = "status-good"; - m_labelMaxMemIcon->setToolTip(""); - } + if (m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked()) + jvmArgs = m_ui->jvmArgsTextBox->toPlainText().replace("\n", " "); + else + jvmArgs = APPLICATION->settings()->get("JvmArgs").toString(); - { - auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - m_labelMaxMemIcon->setPixmap(pix); - } + m_checker.reset(new JavaCommon::TestCheck(this, m_ui->javaPathTextBox->text(), jvmArgs, m_ui->minMemSpinBox->value(), + m_ui->maxMemSpinBox->value(), m_ui->permGenSpinBox->value())); + connect(m_checker.get(), &JavaCommon::TestCheck::finished, this, [this] { m_checker.reset(); }); + m_checker->run(); } -bool JavaSettingsWidget::autoDownloadJava() const +void JavaSettingsWidget::onJavaAutodetect() { - return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); -} + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } -bool JavaSettingsWidget::autoDetectJava() const -{ - return m_autodetectJavaCheckBox->isChecked(); + VersionSelectDialog versionDialog(APPLICATION->javalist(), tr("Select a Java version"), this, true); + versionDialog.setResizeOn(2); + versionDialog.exec(); + + if (versionDialog.result() == QDialog::Accepted && versionDialog.selectedVersion()) { + JavaInstallPtr java = std::dynamic_pointer_cast(versionDialog.selectedVersion()); + m_ui->javaPathTextBox->setText(java->path); + + if (!java->is_64bit && m_ui->maxMemSpinBox->value() > 2048) { + CustomMessageBox::selectable(this, tr("Confirm Selection"), + tr("You selected a 32-bit version of Java.\n" + "This installation does not support more than 2048MiB of RAM.\n" + "Please make sure that the maximum memory value is lower."), + QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) + ->exec(); + } + } } - -void JavaSettingsWidget::onSpinBoxValueChanged(int) +void JavaSettingsWidget::updateThresholds() { - m_memoryTimer->start(500); + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + unsigned int maxMem = m_ui->maxMemSpinBox->value(); + unsigned int minMem = m_ui->minMemSpinBox->value(); + + const QString warningColour(QStringLiteral("%1")); + + if (maxMem >= sysMiB) { + m_ui->labelMaxMemNotice->setText( + QString("%1").arg(tr("Your maximum memory allocation exceeds your system memory capacity."))); + m_ui->labelMaxMemNotice->show(); + } else if (maxMem > (sysMiB * 0.9)) { + m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is close to your system memory capacity."))); + m_ui->labelMaxMemNotice->show(); + } else if (maxMem < minMem) { + m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))); + m_ui->labelMaxMemNotice->show(); + } else { + m_ui->labelMaxMemNotice->hide(); + } } - -JavaSettingsWidget::~JavaSettingsWidget() -{ - delete m_verticalSpacer; -}; \ No newline at end of file diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 35be48478..65154597a 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -1,106 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once -#include -#include -#include -#include -#include -#include +#include +#include "BaseInstance.h" +#include "JavaCommon.h" -class QLineEdit; -class VersionSelectWidget; -class QSpinBox; -class QPushButton; -class QVBoxLayout; -class QHBoxLayout; -class QGroupBox; -class QGridLayout; -class QLabel; -class QToolButton; -class QSpacerItem; +namespace Ui { +class JavaSettingsWidget; +} -/** - * This is a widget for all the Java settings dialogs and pages. - */ class JavaSettingsWidget : public QWidget { Q_OBJECT public: - explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget(); - - enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; - - enum class ValidationStatus { Bad, JavaBad, AllOK }; - - void refresh(); - void initialize(); - ValidationStatus validate(); - void retranslate(); + explicit JavaSettingsWidget(QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, parent) {} + explicit JavaSettingsWidget(BaseInstance* instance, QWidget* parent = nullptr); + ~JavaSettingsWidget() override; - bool permGenEnabled() const; - int permGenSize() const; - int minHeapSize() const; - int maxHeapSize() const; - QString javaPath() const; - bool autoDetectJava() const; - bool autoDownloadJava() const; + void loadSettings(); + void saveSettings(); + private slots: + void onJavaBrowse(); + void onJavaAutodetect(); + void onJavaTest(); void updateThresholds(); - protected slots: - void onSpinBoxValueChanged(int); - void memoryValueChanged(); - void javaPathEdited(const QString& path); - void javaVersionSelected(BaseVersion::Ptr version); - void on_javaBrowseBtn_clicked(); - void on_javaStatusBtn_clicked(); - void javaDownloadBtn_clicked(); - void checkFinished(const JavaChecker::Result& result); - - protected: /* methods */ - void checkJavaPathOnEdit(const QString& path); - void checkJavaPath(const QString& path); - void setJavaStatus(JavaStatus status); - void setupUi(); - - private: /* data */ - VersionSelectWidget* m_versionWidget = nullptr; - QVBoxLayout* m_verticalLayout = nullptr; - QSpacerItem* m_verticalSpacer = nullptr; - - QLineEdit* m_javaPathTextBox = nullptr; - QPushButton* m_javaBrowseBtn = nullptr; - QToolButton* m_javaStatusBtn = nullptr; - QHBoxLayout* m_horizontalLayout = nullptr; - - QGroupBox* m_memoryGroupBox = nullptr; - QGridLayout* m_gridLayout_2 = nullptr; - QSpinBox* m_maxMemSpinBox = nullptr; - QLabel* m_labelMinMem = nullptr; - QLabel* m_labelMaxMem = nullptr; - QLabel* m_labelMaxMemIcon = nullptr; - QSpinBox* m_minMemSpinBox = nullptr; - QLabel* m_labelPermGen = nullptr; - QSpinBox* m_permGenSpinBox = nullptr; - - QHBoxLayout* m_horizontalBtnLayout = nullptr; - QPushButton* m_javaDownloadBtn = nullptr; - QIcon goodIcon; - QIcon yellowIcon; - QIcon badIcon; - - QGroupBox* m_autoJavaGroupBox = nullptr; - QVBoxLayout* m_veriticalJavaLayout = nullptr; - QCheckBox* m_autodetectJavaCheckBox = nullptr; - QCheckBox* m_autodownloadCheckBox = nullptr; - - unsigned int observedMinMemory = 0; - unsigned int observedMaxMemory = 0; - unsigned int observedPermGenMemory = 0; - QString queuedCheck; - uint64_t m_availableMemory = 0ull; - shared_qobject_ptr m_checker; - JavaChecker::Result m_result; - QTimer* m_memoryTimer; + private: + 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 new file mode 100644 index 000000000..46f714b76 --- /dev/null +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -0,0 +1,392 @@ + + + JavaSettingsWidget + + + + 0 + 0 + 500 + 1000 + + + + Form + + + + + + true + + + Java Insta&llation + + + false + + + false + + + + + + Auto-&detect Java version + + + + + + + + + &Detect + + + + + + + &Browse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Test S&ettings + + + + + + + Open Java &Downloader + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Automatically downloads and selects the Java build recommended by Mojang. + + + Auto-download &Mojang Java + + + + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + + + + + + + + + Java &Executable + + + javaPathTextBox + + + + + + + If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup. + + + Skip Java setup prompt on startup + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + + + + true + + + Memor&y + + + false + + + false + + + + + + (-XX:PermSize) + + + + + + + + 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 + + + + + + + + + + true + + + Java Argumen&ts + + + false + + + false + + + + + + + + + + + + javaPathTextBox + javaDetectBtn + javaBrowseBtn + skipCompatibilityCheckBox + skipWizardCheckBox + autodetectJavaCheckBox + autodownloadJavaCheckBox + javaTestBtn + javaDownloadBtn + minMemSpinBox + maxMemSpinBox + permGenSpinBox + jvmArgsTextBox + + + + diff --git a/launcher/ui/widgets/JavaWizardWidget.cpp b/launcher/ui/widgets/JavaWizardWidget.cpp new file mode 100644 index 000000000..ce046e70f --- /dev/null +++ b/launcher/ui/widgets/JavaWizardWidget.cpp @@ -0,0 +1,560 @@ +#include "JavaWizardWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "DesktopServices.h" +#include "FileSystem.h" +#include "JavaCommon.h" +#include "java/JavaChecker.h" +#include "java/JavaInstall.h" +#include "java/JavaInstallList.h" +#include "java/JavaUtils.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" +#include "ui/widgets/VersionSelectWidget.h" + +#include "Application.h" +#include "BuildConfig.h" + +JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) +{ + m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; + + goodIcon = QIcon::fromTheme("status-good"); + yellowIcon = QIcon::fromTheme("status-yellow"); + badIcon = QIcon::fromTheme("status-bad"); + m_memoryTimer = new QTimer(this); + setupUi(); + + connect(m_minMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); + connect(m_maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); + connect(m_permGenSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); + connect(m_memoryTimer, &QTimer::timeout, this, &JavaWizardWidget::memoryValueChanged); + connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaWizardWidget::javaVersionSelected); + connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaWizardWidget::on_javaBrowseBtn_clicked); + connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaWizardWidget::javaPathEdited); + connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaWizardWidget::on_javaStatusBtn_clicked); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaWizardWidget::javaDownloadBtn_clicked); + } +} + +void JavaWizardWidget::setupUi() +{ + setObjectName(QStringLiteral("javaSettingsWidget")); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(this); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + m_javaPathTextBox = new QLineEdit(this); + m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); + + m_horizontalLayout->addWidget(m_javaPathTextBox); + + m_javaBrowseBtn = new QPushButton(this); + m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); + + m_horizontalLayout->addWidget(m_javaBrowseBtn); + + m_javaStatusBtn = new QToolButton(this); + m_javaStatusBtn->setIcon(yellowIcon); + m_horizontalLayout->addWidget(m_javaStatusBtn); + + m_memoryGroupBox = new QGroupBox(this); + m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); + m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); + m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); + m_gridLayout_2->setColumnStretch(0, 1); + + m_labelMinMem = new QLabel(m_memoryGroupBox); + m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); + m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); + + m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); + m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_minMemSpinBox->setMinimum(8); + m_minMemSpinBox->setMaximum(1048576); + m_minMemSpinBox->setSingleStep(128); + m_labelMinMem->setBuddy(m_minMemSpinBox); + m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); + + m_labelMaxMem = new QLabel(m_memoryGroupBox); + m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); + m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); + + m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); + m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_maxMemSpinBox->setMinimum(8); + m_maxMemSpinBox->setMaximum(1048576); + m_maxMemSpinBox->setSingleStep(128); + m_labelMaxMem->setBuddy(m_maxMemSpinBox); + m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); + + m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); + m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); + m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); + + m_labelPermGen = new QLabel(m_memoryGroupBox); + m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); + m_labelPermGen->setText(QStringLiteral("PermGen:")); + m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); + m_labelPermGen->setVisible(false); + + m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); + m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); + m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); + m_permGenSpinBox->setMinimum(4); + m_permGenSpinBox->setMaximum(1048576); + m_permGenSpinBox->setSingleStep(8); + m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); + m_permGenSpinBox->setVisible(false); + + m_verticalLayout->addWidget(m_memoryGroupBox); + + m_horizontalBtnLayout = new QHBoxLayout(); + m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout")); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_javaDownloadBtn = new QPushButton(tr("Download Java"), this); + m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); + } + + m_autoJavaGroupBox = new QGroupBox(this); + m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); + m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); + m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout")); + + m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox); + m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox"); + m_autodetectJavaCheckBox->setChecked(true); + m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox); + m_autodownloadCheckBox->setObjectName("autodownloadCheckBox"); + m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); + m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox); + connect(m_autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { + m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); + if (!m_autodetectJavaCheckBox->isChecked()) + m_autodownloadCheckBox->setChecked(false); + }); + + connect(m_autodownloadCheckBox, &QCheckBox::stateChanged, this, [this] { + auto isChecked = m_autodownloadCheckBox->isChecked(); + m_versionWidget->setVisible(!isChecked); + m_javaStatusBtn->setVisible(!isChecked); + m_javaBrowseBtn->setVisible(!isChecked); + m_javaPathTextBox->setVisible(!isChecked); + m_javaDownloadBtn->setVisible(!isChecked); + if (!isChecked) { + m_verticalLayout->removeItem(m_verticalSpacer); + } else { + m_verticalLayout->addSpacerItem(m_verticalSpacer); + } + }); + } + m_verticalLayout->addWidget(m_autoJavaGroupBox); + + m_verticalLayout->addLayout(m_horizontalBtnLayout); + + m_verticalLayout->addWidget(m_versionWidget); + m_verticalLayout->addLayout(m_horizontalLayout); + m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + + retranslate(); +} + +void JavaWizardWidget::initialize() +{ + m_versionWidget->initialize(APPLICATION->javalist()); + m_versionWidget->selectSearch(); + m_versionWidget->setResizeOn(2); + auto s = APPLICATION->settings(); + // Memory + observedMinMemory = s->get("MinMemAlloc").toInt(); + observedMaxMemory = s->get("MaxMemAlloc").toInt(); + observedPermGenMemory = s->get("PermGen").toInt(); + m_minMemSpinBox->setValue(observedMinMemory); + m_maxMemSpinBox->setValue(observedMaxMemory); + m_permGenSpinBox->setValue(observedPermGenMemory); + updateThresholds(); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setChecked(true); + } +} + +void JavaWizardWidget::refresh() +{ + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + return; + } + if (JavaUtils::getJavaCheckPath().isEmpty()) { + JavaCommon::javaCheckNotFound(this); + return; + } + m_versionWidget->loadList(); +} + +JavaWizardWidget::ValidationStatus JavaWizardWidget::validate() +{ + switch (javaStatus) { + default: + case JavaStatus::NotSet: + /* fallthrough */ + case JavaStatus::DoesNotExist: + /* fallthrough */ + case JavaStatus::DoesNotStart: + /* fallthrough */ + case JavaStatus::ReturnedInvalidData: { + if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())) { // the java will not be autodownloaded + int button = QMessageBox::No; + if (m_result.mojangPlatform == "32" && maxHeapSize() > 2048) { + button = CustomMessageBox::selectable( + this, tr("32-bit Java detected"), + tr("You selected a 32-bit installation of Java, but allocated more than 2048MiB as maximum memory.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) + ->exec(); + + } else { + button = CustomMessageBox::selectable(this, tr("No Java version selected"), + tr("You either didn't select a Java version or selected one that does not work.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed without a functional version of Java?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, + QMessageBox::NoButton) + ->exec(); + } + switch (button) { + case QMessageBox::Yes: + return ValidationStatus::JavaBad; + case QMessageBox::Help: + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); + /* fallthrough */ + case QMessageBox::No: + /* fallthrough */ + default: + return ValidationStatus::Bad; + } + if (button == QMessageBox::No) { + return ValidationStatus::Bad; + } + } + return ValidationStatus::JavaBad; + } break; + case JavaStatus::Pending: { + return ValidationStatus::Bad; + } + case JavaStatus::Good: { + return ValidationStatus::AllOK; + } + } +} + +QString JavaWizardWidget::javaPath() const +{ + return m_javaPathTextBox->text(); +} + +int JavaWizardWidget::maxHeapSize() const +{ + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (max < min) + max = min; + return max; +} + +int JavaWizardWidget::minHeapSize() const +{ + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (min > max) + min = max; + return min; +} + +bool JavaWizardWidget::permGenEnabled() const +{ + return m_permGenSpinBox->isVisible(); +} + +int JavaWizardWidget::permGenSize() const +{ + return m_permGenSpinBox->value(); +} + +void JavaWizardWidget::memoryValueChanged() +{ + bool actuallyChanged = false; + unsigned int min = m_minMemSpinBox->value(); + unsigned int max = m_maxMemSpinBox->value(); + unsigned int permgen = m_permGenSpinBox->value(); + if (min != observedMinMemory) { + observedMinMemory = min; + actuallyChanged = true; + } + if (max != observedMaxMemory) { + observedMaxMemory = max; + actuallyChanged = true; + } + if (permgen != observedPermGenMemory) { + observedPermGenMemory = permgen; + actuallyChanged = true; + } + if (actuallyChanged) { + checkJavaPathOnEdit(m_javaPathTextBox->text()); + updateThresholds(); + } +} + +void JavaWizardWidget::javaVersionSelected(BaseVersion::Ptr version) +{ + auto java = std::dynamic_pointer_cast(version); + if (!java) { + return; + } + auto visible = java->id.requiresPermGen(); + m_labelPermGen->setVisible(visible); + m_permGenSpinBox->setVisible(visible); + m_javaPathTextBox->setText(java->path); + checkJavaPath(java->path); +} + +void JavaWizardWidget::on_javaBrowseBtn_clicked() +{ + auto filter = QString("Java (%1)").arg(JavaUtils::javaExecutable); + auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); + if (raw_path.isEmpty()) { + return; + } + auto cooked_path = FS::NormalizePath(raw_path); + m_javaPathTextBox->setText(cooked_path); + checkJavaPath(cooked_path); +} + +void JavaWizardWidget::javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, nullptr, this); + jdialog->exec(); +} + +void JavaWizardWidget::on_javaStatusBtn_clicked() +{ + QString text; + bool failed = false; + switch (javaStatus) { + case JavaStatus::NotSet: + checkJavaPath(m_javaPathTextBox->text()); + return; + case JavaStatus::DoesNotExist: + text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); + failed = true; + break; + case JavaStatus::DoesNotStart: { + text += QObject::tr("The specified Java binary didn't start properly.
    "); + auto htmlError = m_result.errorLog; + if (!htmlError.isEmpty()) { + htmlError.replace('\n', "
    "); + text += QString("%1").arg(htmlError); + } + failed = true; + break; + } + case JavaStatus::ReturnedInvalidData: { + text += QObject::tr("The specified Java binary returned unexpected results:
    "); + auto htmlOut = m_result.outLog; + if (!htmlOut.isEmpty()) { + htmlOut.replace('\n', "
    "); + text += QString("%1").arg(htmlOut); + } + failed = true; + break; + } + case JavaStatus::Good: + text += QObject::tr( + "Java test succeeded!
    Platform reported: %1
    Java version " + "reported: %2
    ") + .arg(m_result.realPlatform, m_result.javaVersion.toString()); + break; + case JavaStatus::Pending: + // TODO: abort here? + return; + } + CustomMessageBox::selectable(this, failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, + failed ? QMessageBox::Critical : QMessageBox::Information) + ->show(); +} + +void JavaWizardWidget::setJavaStatus(JavaWizardWidget::JavaStatus status) +{ + javaStatus = status; + switch (javaStatus) { + case JavaStatus::Good: + m_javaStatusBtn->setIcon(goodIcon); + break; + case JavaStatus::NotSet: + case JavaStatus::Pending: + m_javaStatusBtn->setIcon(yellowIcon); + break; + default: + m_javaStatusBtn->setIcon(badIcon); + break; + } +} + +void JavaWizardWidget::javaPathEdited(const QString& path) +{ + checkJavaPathOnEdit(path); +} + +void JavaWizardWidget::checkJavaPathOnEdit(const QString& path) +{ + auto realPath = FS::ResolveExecutable(path); + QFileInfo pathInfo(realPath); + if (pathInfo.baseName().toLower().contains("java")) { + checkJavaPath(path); + } else { + if (!m_checker) { + setJavaStatus(JavaStatus::NotSet); + } + } +} + +void JavaWizardWidget::checkJavaPath(const QString& path) +{ + if (m_checker) { + queuedCheck = path; + return; + } + auto realPath = FS::ResolveExecutable(path); + if (realPath.isNull()) { + setJavaStatus(JavaStatus::DoesNotExist); + return; + } + setJavaStatus(JavaStatus::Pending); + m_checker.reset( + new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0)); + connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaWizardWidget::checkFinished); + m_checker->start(); +} + +void JavaWizardWidget::checkFinished(const JavaChecker::Result& result) +{ + m_result = result; + switch (result.validity) { + case JavaChecker::Result::Validity::Valid: { + setJavaStatus(JavaStatus::Good); + break; + } + case JavaChecker::Result::Validity::ReturnedInvalidData: { + setJavaStatus(JavaStatus::ReturnedInvalidData); + break; + } + case JavaChecker::Result::Validity::Errored: { + setJavaStatus(JavaStatus::DoesNotStart); + break; + } + } + updateThresholds(); + m_checker.reset(); + if (!queuedCheck.isNull()) { + checkJavaPath(queuedCheck); + queuedCheck.clear(); + } +} + +void JavaWizardWidget::retranslate() +{ + m_memoryGroupBox->setTitle(tr("Memory")); + m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); + m_labelMinMem->setText(tr("Minimum memory allocation:")); + m_labelMaxMem->setText(tr("Maximum memory allocation:")); + m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); + m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); + m_javaBrowseBtn->setText(tr("Browse")); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); + } + m_autodetectJavaCheckBox->setText(tr("Auto-detect Java version")); + m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); +} + +void JavaWizardWidget::updateThresholds() +{ + QString iconName; + + if (observedMaxMemory >= m_availableMemory) { + iconName = "status-bad"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + } else if (observedMaxMemory > (m_availableMemory * 0.9)) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (observedMaxMemory < observedMinMemory) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + } else if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { + iconName = "status-good"; + m_labelMaxMemIcon->setToolTip(""); + } else if (observedMaxMemory > 2048 && !m_result.is_64bit) { + iconName = "status-bad"; + m_labelMaxMemIcon->setToolTip(tr("You are exceeding the maximum allocation supported by 32-bit installations of Java.")); + } else { + iconName = "status-good"; + m_labelMaxMemIcon->setToolTip(""); + } + + { + auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); + QIcon icon = QIcon::fromTheme(iconName); + QPixmap pix = icon.pixmap(height, height); + m_labelMaxMemIcon->setPixmap(pix); + } +} + +bool JavaWizardWidget::autoDownloadJava() const +{ + return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); +} + +bool JavaWizardWidget::autoDetectJava() const +{ + return m_autodetectJavaCheckBox->isChecked(); +} + +void JavaWizardWidget::onSpinBoxValueChanged(int) +{ + m_memoryTimer->start(500); +} + +JavaWizardWidget::~JavaWizardWidget() +{ + delete m_verticalSpacer; +}; diff --git a/launcher/ui/widgets/JavaWizardWidget.h b/launcher/ui/widgets/JavaWizardWidget.h new file mode 100644 index 000000000..4a877d867 --- /dev/null +++ b/launcher/ui/widgets/JavaWizardWidget.h @@ -0,0 +1,103 @@ +#pragma once +#include + +#include +#include +#include +#include + +class QCheckBox; +class QLineEdit; +class VersionSelectWidget; +class QSpinBox; +class QPushButton; +class QVBoxLayout; +class QHBoxLayout; +class QGroupBox; +class QGridLayout; +class QLabel; +class QToolButton; +class QSpacerItem; + +class JavaWizardWidget : public QWidget { + Q_OBJECT + + public: + explicit JavaWizardWidget(QWidget* parent); + virtual ~JavaWizardWidget(); + + enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; + + enum class ValidationStatus { Bad, JavaBad, AllOK }; + + void refresh(); + void initialize(); + ValidationStatus validate(); + void retranslate(); + + bool permGenEnabled() const; + int permGenSize() const; + int minHeapSize() const; + int maxHeapSize() const; + QString javaPath() const; + bool autoDetectJava() const; + bool autoDownloadJava() const; + + void updateThresholds(); + + protected slots: + void onSpinBoxValueChanged(int); + void memoryValueChanged(); + void javaPathEdited(const QString& path); + void javaVersionSelected(BaseVersion::Ptr version); + void on_javaBrowseBtn_clicked(); + void on_javaStatusBtn_clicked(); + void javaDownloadBtn_clicked(); + void checkFinished(const JavaChecker::Result& result); + + protected: /* methods */ + void checkJavaPathOnEdit(const QString& path); + void checkJavaPath(const QString& path); + void setJavaStatus(JavaStatus status); + void setupUi(); + + private: /* data */ + VersionSelectWidget* m_versionWidget = nullptr; + QVBoxLayout* m_verticalLayout = nullptr; + QSpacerItem* m_verticalSpacer = nullptr; + + QLineEdit* m_javaPathTextBox = nullptr; + QPushButton* m_javaBrowseBtn = nullptr; + QToolButton* m_javaStatusBtn = nullptr; + QHBoxLayout* m_horizontalLayout = nullptr; + + QGroupBox* m_memoryGroupBox = nullptr; + QGridLayout* m_gridLayout_2 = nullptr; + QSpinBox* m_maxMemSpinBox = nullptr; + QLabel* m_labelMinMem = nullptr; + QLabel* m_labelMaxMem = nullptr; + QLabel* m_labelMaxMemIcon = nullptr; + QSpinBox* m_minMemSpinBox = nullptr; + QLabel* m_labelPermGen = nullptr; + QSpinBox* m_permGenSpinBox = nullptr; + + QHBoxLayout* m_horizontalBtnLayout = nullptr; + QPushButton* m_javaDownloadBtn = nullptr; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + + QGroupBox* m_autoJavaGroupBox = nullptr; + QVBoxLayout* m_veriticalJavaLayout = nullptr; + QCheckBox* m_autodetectJavaCheckBox = nullptr; + QCheckBox* m_autodownloadCheckBox = nullptr; + + unsigned int observedMinMemory = 0; + unsigned int observedMaxMemory = 0; + unsigned int observedPermGenMemory = 0; + QString queuedCheck; + uint64_t m_availableMemory = 0ull; + shared_qobject_ptr m_checker; + JavaChecker::Result m_result; + QTimer* m_memoryTimer; +}; diff --git a/launcher/ui/widgets/LanguageSelectionWidget.cpp b/launcher/ui/widgets/LanguageSelectionWidget.cpp index 481547b5b..4ca416aad 100644 --- a/launcher/ui/widgets/LanguageSelectionWidget.cpp +++ b/launcher/ui/widgets/LanguageSelectionWidget.cpp @@ -40,7 +40,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); diff --git a/launcher/ui/widgets/LineSeparator.cpp b/launcher/ui/widgets/LineSeparator.cpp deleted file mode 100644 index 2d6239a2f..000000000 --- a/launcher/ui/widgets/LineSeparator.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "LineSeparator.h" - -#include -#include -#include -#include - -void LineSeparator::initStyleOption(QStyleOption* option) const -{ - option->initFrom(this); - // in a horizontal layout, the line is vertical (and vice versa) - if (m_orientation == Qt::Vertical) - option->state |= QStyle::State_Horizontal; -} - -LineSeparator::LineSeparator(QWidget* parent, Qt::Orientation orientation) : QWidget(parent), m_orientation(orientation) -{ - setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); -} - -QSize LineSeparator::sizeHint() const -{ - QStyleOption opt; - initStyleOption(&opt); - const int extent = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget()); - return QSize(extent, extent); -} - -void LineSeparator::paintEvent(QPaintEvent*) -{ - QPainter p(this); - QStyleOption opt; - initStyleOption(&opt); - style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); -} diff --git a/launcher/ui/widgets/LineSeparator.h b/launcher/ui/widgets/LineSeparator.h deleted file mode 100644 index 719facb99..000000000 --- a/launcher/ui/widgets/LineSeparator.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include - -class QStyleOption; - -class LineSeparator : public QWidget { - Q_OBJECT - - public: - /// Create a line separator. orientation is the orientation of the line. - explicit LineSeparator(QWidget* parent, Qt::Orientation orientation = Qt::Horizontal); - QSize sizeHint() const; - void paintEvent(QPaintEvent*); - void initStyleOption(QStyleOption* option) const; - - private: - Qt::Orientation m_orientation = Qt::Horizontal; -}; diff --git a/launcher/ui/widgets/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 6578b1f12..73496a01a 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -42,6 +42,7 @@ LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) { setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); m_defaultFormat = new QTextCharFormat(currentCharFormat()); + setUndoRedoEnabled(false); } LogView::~LogView() @@ -60,6 +61,14 @@ void LogView::setWordWrap(bool wrapping) } } +void LogView::setColorLines(bool colorLines) +{ + if (m_colorLines == colorLines) + return; + m_colorLines = colorLines; + repopulate(); +} + void LogView::setModel(QAbstractItemModel* model) { if (m_model) { @@ -121,6 +130,8 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) QTextDocument document; QTextCursor cursor(&document); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); for (int i = first; i <= last; i++) { auto idx = m_model->index(i, 0, parent); auto text = m_model->data(idx, Qt::DisplayRole).toString(); @@ -130,17 +141,17 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) format.setFont(font.value()); } auto fg = m_model->data(idx, Qt::ForegroundRole); - if (fg.isValid()) { + if (fg.isValid() && m_colorLines) { format.setForeground(fg.value()); } auto bg = m_model->data(idx, Qt::BackgroundRole); - if (bg.isValid()) { + if (bg.isValid() && m_colorLines) { format.setBackground(bg.value()); } - cursor.movePosition(QTextCursor::End); cursor.insertText(text, format); cursor.insertBlock(); } + cursor.endEditBlock(); QTextDocumentFragment fragment(&document); QTextCursor workCursor = textCursor(); @@ -169,5 +180,30 @@ void LogView::scrollToBottom() void LogView::findNext(const QString& what, bool reverse) { - find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); + if (what.isEmpty()) + return; + + const QTextDocument::FindFlags flags(reverse ? QTextDocument::FindBackward : 0); + + if (find(what, flags)) + return; + + QTextCursor cursor = textCursor(); + + if (reverse) { + if (cursor.atEnd()) + return; + + cursor.movePosition(QTextCursor::End); + } else { + if (cursor.atStart()) + return; + + cursor.movePosition(QTextCursor::Start); + } + + cursor = document()->find(what, cursor, flags); + + if (!cursor.isNull()) + setTextCursor(cursor); } diff --git a/launcher/ui/widgets/LogView.h b/launcher/ui/widgets/LogView.h index dde5f8f76..69ca332bb 100644 --- a/launcher/ui/widgets/LogView.h +++ b/launcher/ui/widgets/LogView.h @@ -15,6 +15,7 @@ class LogView : public QPlainTextEdit { public slots: void setWordWrap(bool wrapping); + void setColorLines(bool colorLines); void findNext(const QString& what, bool reverse); void scrollToBottom(); @@ -32,4 +33,5 @@ class LogView : public QPlainTextEdit { QTextCharFormat* m_defaultFormat = nullptr; bool m_scroll = false; bool m_scrolling = false; + bool m_colorLines = true; }; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp new file mode 100644 index 000000000..255d19317 --- /dev/null +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2024 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftSettingsWidget.h" +#include "modplatform/ModIndex.h" +#include "ui_MinecraftSettingsWidget.h" + +#include +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" +#include "minecraft/PackProfile.h" +#include "minecraft/WorldList.h" +#include "minecraft/auth/AccountList.h" +#include "settings/Setting.h" + +MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstance* instance, QWidget* parent) + : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) +{ + m_ui->setupUi(this); + + if (m_instance == nullptr) { + m_ui->settingsTabs->removeTab(1); + + m_ui->openGlobalSettingsButton->setVisible(false); + m_ui->instanceAccountGroupBox->hide(); + m_ui->serverJoinGroupBox->hide(); + m_ui->globalDataPacksGroupBox->hide(); + m_ui->loaderGroup->hide(); + } else { + m_javaSettings = new JavaSettingsWidget(m_instance, this); + m_ui->javaScrollArea->setWidget(m_javaSettings); + + m_ui->showGameTime->setText(tr("Show time &playing this instance")); + m_ui->recordGameTime->setText(tr("&Record time playing this instance")); + m_ui->showGlobalGameTime->hide(); + m_ui->showGameTimeWithoutDays->hide(); + + m_ui->maximizedWarning->setText( + tr("Warning: The maximized option is " + "not fully supported on this Minecraft version.")); + + m_ui->consoleSettingsBox->setCheckable(true); + m_ui->windowSizeGroupBox->setCheckable(true); + m_ui->nativeWorkaroundsGroupBox->setCheckable(true); + m_ui->perfomanceGroupBox->setCheckable(true); + m_ui->gameTimeGroupBox->setCheckable(true); + m_ui->legacySettingsGroupBox->setCheckable(true); + + m_quickPlaySingleplayer = m_instance->traits().contains("feature:is_quick_play_singleplayer"); + if (m_quickPlaySingleplayer) { + auto worlds = m_instance->worldList(); + worlds->update(); + for (const auto& world : worlds->allWorlds()) { + m_ui->worldsCb->addItem(world.folderName()); + } + } else { + m_ui->worldsCb->hide(); + m_ui->worldJoinButton->hide(); + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); + } + + connect(m_ui->openGlobalSettingsButton, &QCommandLinkButton::clicked, this, &MinecraftSettingsWidget::openGlobalSettings); + connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); + connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); + + connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("GlobalDataPacksEnabled", value); + if (!value) + m_instance->settings()->reset("GlobalDataPacksPath"); + }); + connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::saveDataPacksPath); + connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); + + connect(m_ui->loaderGroup, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("OverrideModDownloadLoaders", value); + if (value) + saveSelectedLoaders(); + else + m_instance->settings()->reset("ModDownloadLoaders"); + }); + connect(m_ui->neoForge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->forge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->fabric, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->quilt, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->liteLoader, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + } + + m_ui->maximizedWarning->hide(); + + connect(m_ui->maximizedCheckBox, &QCheckBox::toggled, this, + [this](const bool value) { m_ui->maximizedWarning->setVisible(value && (m_instance == nullptr || !m_instance->isLegacy())); }); + +#if !defined(Q_OS_LINUX) + m_ui->perfomanceGroupBox->hide(); +#endif + + if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { + m_ui->enableFeralGamemodeCheck->setDisabled(true); + m_ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); + } + + if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { + m_ui->enableMangoHud->setEnabled(false); + m_ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); + } + + connect(m_ui->useNativeOpenALCheck, &QAbstractButton::toggled, m_ui->lineEditOpenALPath, &QWidget::setEnabled); + connect(m_ui->useNativeGLFWCheck, &QAbstractButton::toggled, m_ui->lineEditGLFWPath, &QWidget::setEnabled); + + loadSettings(); +} + +MinecraftSettingsWidget::~MinecraftSettingsWidget() +{ + delete m_ui; +} + +void MinecraftSettingsWidget::loadSettings() +{ + SettingsObject* settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + // Game Window + m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool() || + settings->get("OverrideMiscellaneous").toBool()); + m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool()); + m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt()); + m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt()); + m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool()); + m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool()); + + // Game Time + m_ui->gameTimeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideGameTime").toBool()); + m_ui->showGameTime->setChecked(settings->get("ShowGameTime").toBool()); + m_ui->recordGameTime->setChecked(settings->get("RecordGameTime").toBool()); + m_ui->showGlobalGameTime->setChecked(m_instance == nullptr && settings->get("ShowGlobalGameTime").toBool()); + m_ui->showGameTimeWithoutDays->setChecked(m_instance == nullptr && settings->get("ShowGameTimeWithoutDays").toBool()); + + // Console + m_ui->consoleSettingsBox->setChecked(m_instance == nullptr || settings->get("OverrideConsole").toBool()); + m_ui->showConsoleCheck->setChecked(settings->get("ShowConsole").toBool()); + m_ui->autoCloseConsoleCheck->setChecked(settings->get("AutoCloseConsole").toBool()); + m_ui->showConsoleErrorCheck->setChecked(settings->get("ShowConsoleOnError").toBool()); + + if (m_javaSettings != nullptr) + m_javaSettings->loadSettings(); + + // Custom commands + m_ui->customCommands->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideCommands").toBool(), + settings->get("PreLaunchCommand").toString(), settings->get("WrapperCommand").toString(), + settings->get("PostExitCommand").toString()); + + // Environment variables + m_ui->environmentVariables->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideEnv").toBool(), + Json::toMap(settings->get("Env").toString())); + + // Legacy Tweaks + m_ui->legacySettingsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideLegacySettings").toBool()); + m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); + + // Native Libraries + m_ui->nativeWorkaroundsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideNativeWorkarounds").toBool()); + m_ui->useNativeGLFWCheck->setChecked(settings->get("UseNativeGLFW").toBool()); + m_ui->lineEditGLFWPath->setText(settings->get("CustomGLFWPath").toString()); +#ifdef Q_OS_LINUX + m_ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); +#else + m_ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); +#endif + m_ui->useNativeOpenALCheck->setChecked(settings->get("UseNativeOpenAL").toBool()); + m_ui->lineEditOpenALPath->setText(settings->get("CustomOpenALPath").toString()); +#ifdef Q_OS_LINUX + m_ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); +#else + m_ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); +#endif + + // Performance + m_ui->perfomanceGroupBox->setChecked(m_instance == nullptr || settings->get("OverridePerformance").toBool()); + m_ui->enableFeralGamemodeCheck->setChecked(settings->get("EnableFeralGamemode").toBool()); + m_ui->enableMangoHud->setChecked(settings->get("EnableMangoHud").toBool()); + m_ui->useDiscreteGpuCheck->setChecked(settings->get("UseDiscreteGpu").toBool()); + m_ui->useZink->setChecked(settings->get("UseZink").toBool()); + + if (m_instance != nullptr) { + // HACK: if we change enable state of child widgets while it's unchecked this creates inconsistency + m_ui->serverJoinGroupBox->setChecked(true); + + if (auto server = settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { + m_ui->serverJoinAddress->setText(server); + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->worldJoinButton->setChecked(false); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->worldsCb->setEnabled(false); + } else if (auto world = settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_quickPlaySingleplayer) { + m_ui->worldsCb->setCurrentText(world); + m_ui->serverJoinAddressButton->setChecked(false); + m_ui->worldJoinButton->setChecked(true); + m_ui->serverJoinAddress->setEnabled(false); + m_ui->worldsCb->setEnabled(true); + } else { + m_ui->serverJoinAddressButton->setChecked(true); + m_ui->worldJoinButton->setChecked(false); + m_ui->serverJoinAddress->setEnabled(true); + m_ui->worldsCb->setEnabled(false); + } + + m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); + + m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool()); + updateAccountsMenu(*settings); + + m_ui->loaderGroup->blockSignals(true); + m_ui->neoForge->blockSignals(true); + m_ui->forge->blockSignals(true); + m_ui->fabric->blockSignals(true); + m_ui->quilt->blockSignals(true); + m_ui->liteLoader->blockSignals(true); + + const bool overrideLoaders = settings->get("OverrideModDownloadLoaders").toBool(); + const QStringList loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString()); + + m_ui->loaderGroup->setChecked(overrideLoaders); + + if (overrideLoaders) { + m_ui->neoForge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::NeoForge))); + m_ui->forge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Forge))); + m_ui->fabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Fabric))); + m_ui->quilt->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Quilt))); + m_ui->liteLoader->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LiteLoader))); + } else { + auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes(0)); + + m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); + m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); + m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric); + m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt); + m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader); + } + + m_ui->loaderGroup->blockSignals(false); + m_ui->neoForge->blockSignals(false); + m_ui->forge->blockSignals(false); + m_ui->fabric->blockSignals(false); + m_ui->quilt->blockSignals(false); + m_ui->liteLoader->blockSignals(false); + } + + m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); + m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); + + m_ui->globalDataPacksGroupBox->blockSignals(true); + m_ui->dataPacksPathEdit->blockSignals(true); + m_ui->globalDataPacksGroupBox->setChecked(settings->get("GlobalDataPacksEnabled").toBool()); + m_ui->dataPacksPathEdit->setText(settings->get("GlobalDataPacksPath").toString()); + m_ui->globalDataPacksGroupBox->blockSignals(false); + m_ui->dataPacksPathEdit->blockSignals(false); +} + +void MinecraftSettingsWidget::saveSettings() +{ + SettingsObject* settings; + + if (m_instance != nullptr) + settings = m_instance->settings(); + else + settings = APPLICATION->settings(); + + { + SettingsObject::Lock lock(settings); + + // Console + bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideConsole", console); + + if (console) { + settings->set("ShowConsole", m_ui->showConsoleCheck->isChecked()); + settings->set("AutoCloseConsole", m_ui->autoCloseConsoleCheck->isChecked()); + settings->set("ShowConsoleOnError", m_ui->showConsoleErrorCheck->isChecked()); + } else { + settings->reset("ShowConsole"); + settings->reset("AutoCloseConsole"); + settings->reset("ShowConsoleOnError"); + } + + // Game Window + bool window = m_instance == nullptr || m_ui->windowSizeGroupBox->isChecked(); + + if (m_instance != nullptr) { + settings->set("OverrideWindow", window); + settings->set("OverrideMiscellaneous", window); + } + + if (window) { + settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked()); + settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value()); + settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value()); + settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); + settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); + } else { + settings->reset("LaunchMaximized"); + settings->reset("MinecraftWinWidth"); + settings->reset("MinecraftWinHeight"); + settings->reset("CloseAfterLaunch"); + settings->reset("QuitAfterGameStop"); + } + + // Custom Commands + bool custcmd = m_instance == nullptr || m_ui->customCommands->checked(); + + if (m_instance != nullptr) + settings->set("OverrideCommands", custcmd); + + if (custcmd) { + settings->set("PreLaunchCommand", m_ui->customCommands->prelaunchCommand()); + settings->set("WrapperCommand", m_ui->customCommands->wrapperCommand()); + settings->set("PostExitCommand", m_ui->customCommands->postexitCommand()); + } else { + settings->reset("PreLaunchCommand"); + settings->reset("WrapperCommand"); + settings->reset("PostExitCommand"); + } + + // Environment Variables + auto env = m_instance == nullptr || m_ui->environmentVariables->override(); + + if (m_instance != nullptr) + settings->set("OverrideEnv", env); + + if (env) + settings->set("Env", Json::fromMap(m_ui->environmentVariables->value())); + else + settings->reset("Env"); + + // Workarounds + bool workarounds = m_instance == nullptr || m_ui->nativeWorkaroundsGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideNativeWorkarounds", workarounds); + + if (workarounds) { + settings->set("UseNativeGLFW", m_ui->useNativeGLFWCheck->isChecked()); + settings->set("CustomGLFWPath", m_ui->lineEditGLFWPath->text()); + settings->set("UseNativeOpenAL", m_ui->useNativeOpenALCheck->isChecked()); + settings->set("CustomOpenALPath", m_ui->lineEditOpenALPath->text()); + } else { + settings->reset("UseNativeGLFW"); + settings->reset("CustomGLFWPath"); + settings->reset("UseNativeOpenAL"); + settings->reset("CustomOpenALPath"); + } + + // Performance + bool performance = m_instance == nullptr || m_ui->perfomanceGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverridePerformance", performance); + + if (performance) { + settings->set("EnableFeralGamemode", m_ui->enableFeralGamemodeCheck->isChecked()); + settings->set("EnableMangoHud", m_ui->enableMangoHud->isChecked()); + settings->set("UseDiscreteGpu", m_ui->useDiscreteGpuCheck->isChecked()); + settings->set("UseZink", m_ui->useZink->isChecked()); + } else { + settings->reset("EnableFeralGamemode"); + settings->reset("EnableMangoHud"); + settings->reset("UseDiscreteGpu"); + settings->reset("UseZink"); + } + + // Game time + bool gameTime = m_instance == nullptr || m_ui->gameTimeGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideGameTime", gameTime); + + if (gameTime) { + settings->set("ShowGameTime", m_ui->showGameTime->isChecked()); + settings->set("RecordGameTime", m_ui->recordGameTime->isChecked()); + } else { + settings->reset("ShowGameTime"); + settings->reset("RecordGameTime"); + } + + if (m_instance == nullptr) { + settings->set("ShowGlobalGameTime", m_ui->showGlobalGameTime->isChecked()); + settings->set("ShowGameTimeWithoutDays", m_ui->showGameTimeWithoutDays->isChecked()); + } + + if (m_instance != nullptr) { + // Join server on launch + bool joinServerOnLaunch = m_ui->serverJoinGroupBox->isChecked(); + settings->set("JoinServerOnLaunch", joinServerOnLaunch); + if (joinServerOnLaunch) { + if (m_ui->serverJoinAddressButton->isChecked() || !m_quickPlaySingleplayer) { + settings->set("JoinServerOnLaunchAddress", m_ui->serverJoinAddress->text()); + settings->reset("JoinWorldOnLaunch"); + } else { + settings->set("JoinWorldOnLaunch", m_ui->worldsCb->currentText()); + settings->reset("JoinServerOnLaunchAddress"); + } + } else { + settings->reset("JoinServerOnLaunchAddress"); + settings->reset("JoinWorldOnLaunch"); + } + + // Use an account for this instance + bool useAccountForInstance = m_ui->instanceAccountGroupBox->isChecked(); + settings->set("UseAccountForInstance", useAccountForInstance); + if (useAccountForInstance) { + int accountIndex = m_ui->instanceAccountSelector->currentIndex(); + + if (accountIndex != -1) { + const MinecraftAccountPtr account = APPLICATION->accounts()->at(accountIndex); + if (account != nullptr) + settings->set("InstanceAccountId", account->profileId()); + } + } else { + settings->reset("InstanceAccountId"); + } + } + + bool overrideLegacySettings = m_instance == nullptr || m_ui->legacySettingsGroupBox->isChecked(); + + if (m_instance != nullptr) + settings->set("OverrideLegacySettings", overrideLegacySettings); + + if (overrideLegacySettings) { + settings->set("OnlineFixes", m_ui->onlineFixes->isChecked()); + } else { + settings->reset("OnlineFixes"); + } + } + + if (m_javaSettings != nullptr) + m_javaSettings->saveSettings(); +} + +void MinecraftSettingsWidget::openGlobalSettings() +{ + const QString id = m_ui->settingsTabs->currentWidget()->objectName(); + + qDebug() << id; + + if (id == "javaPage") + APPLICATION->ShowGlobalSettings(this, "java-settings"); + else // TODO select tab + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); +} + +void MinecraftSettingsWidget::updateAccountsMenu(SettingsObject& settings) +{ + m_ui->instanceAccountSelector->clear(); + auto accounts = APPLICATION->accounts(); + int accountIndex = accounts->findAccountByProfileId(settings.get("InstanceAccountId").toString()); + + for (int i = 0; i < accounts->count(); i++) { + MinecraftAccountPtr account = accounts->at(i); + + QIcon face = account->getFace(); + + if (face.isNull()) + face = QIcon::fromTheme("noaccount"); + + m_ui->instanceAccountSelector->addItem(face, account->profileName(), i); + if (i == accountIndex) + m_ui->instanceAccountSelector->setCurrentIndex(i); + } +} + +bool MinecraftSettingsWidget::isQuickPlaySupported() +{ + return m_instance->traits().contains("feature:is_quick_play_singleplayer"); +} + +void MinecraftSettingsWidget::saveSelectedLoaders() +{ + QStringList loaders; + + if (m_ui->neoForge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::NeoForge); + + if (m_ui->forge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Forge); + + if (m_ui->fabric->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Fabric); + + if (m_ui->quilt->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Quilt); + + if (m_ui->liteLoader->isChecked()) + loaders << getModLoaderAsString(ModPlatform::LiteLoader); + + m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); +} + +void MinecraftSettingsWidget::saveDataPacksPath() +{ + if (QDir::separator() != '/') + m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/')); + + m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); +} + +void MinecraftSettingsWidget::selectDataPacksFolder() +{ + QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot()); + + if (path.isEmpty()) + return; + + // if it's inside the instance dir, set path relative to .minecraft + // (so that if it's directly in instance dir it will still lead with .. but more than two levels up are kept absolute) + + const QUrl instanceRootUrl = QUrl::fromLocalFile(m_instance->instanceRoot()); + const QUrl pathUrl = QUrl::fromLocalFile(path); + + if (instanceRootUrl.isParentOf(pathUrl)) + path = QDir(m_instance->gameRoot()).relativeFilePath(path); + + m_ui->dataPacksPathEdit->setText(path); + m_instance->settings()->set("GlobalDataPacksPath", path); +} diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.h similarity index 62% rename from launcher/ui/pages/instance/GameOptionsPage.cpp rename to launcher/ui/widgets/MinecraftSettingsWidget.h index 8db392b1d..847e05806 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,42 +34,35 @@ * limitations under the License. */ -#include "GameOptionsPage.h" +#pragma once + +#include +#include "JavaSettingsWidget.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/gameoptions/GameOptions.h" -#include "ui_GameOptionsPage.h" -GameOptionsPage::GameOptionsPage(MinecraftInstance* inst, QWidget* parent) : QWidget(parent), ui(new Ui::GameOptionsPage) -{ - ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - m_model = inst->gameOptionsModel(); - ui->optionsView->setModel(m_model.get()); - auto head = ui->optionsView->header(); - if (head->count()) { - head->setSectionResizeMode(0, QHeaderView::ResizeToContents); - for (int i = 1; i < head->count(); i++) { - head->setSectionResizeMode(i, QHeaderView::Stretch); - } - } +namespace Ui { +class MinecraftSettingsWidget; } -GameOptionsPage::~GameOptionsPage() -{ - // m_model->save(); -} +class MinecraftSettingsWidget : public QWidget { + public: + MinecraftSettingsWidget(MinecraftInstance* instance, QWidget* parent = nullptr); + ~MinecraftSettingsWidget() override; -void GameOptionsPage::openedImpl() -{ - // m_model->observe(); -} + void loadSettings(); + void saveSettings(); -void GameOptionsPage::closedImpl() -{ - // m_model->unobserve(); -} + private: + void openGlobalSettings(); + void updateAccountsMenu(SettingsObject& settings); + bool isQuickPlaySupported(); + private slots: + void saveSelectedLoaders(); + void saveDataPacksPath(); + void selectDataPacksFolder(); -void GameOptionsPage::retranslate() -{ - ui->retranslateUi(this); -} + 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 new file mode 100644 index 000000000..f8ee2f854 --- /dev/null +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -0,0 +1,859 @@ + + + MinecraftSettingsWidget + + + + 0 + 0 + 648 + 600 + + + + + 0 + + + 0 + + + 6 + + + 0 + + + + + Open &Global Settings + + + The settings here are overrides for global settings. + + + + + + + 0 + + + + General + + + + + + + 0 + 0 + + + + true + + + + + 0 + 0 + 603 + 1042 + + + + + + + true + + + Game &Window + + + false + + + false + + + + + + The base game only supports resolution. In order to simulate the maximized behaviour the current implementation approximates the maximum display size. + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html> + + + + + + + When the game window closes, quit the launcher + + + + + + + Start Minecraft maximized + + + + + + + When the game window opens, hide the launcher + + + + + + + + 0 + 0 + + + + + + + 1 + + + 65536 + + + 1 + + + 854 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + + 0 + 0 + + + + + + + 1 + + + 65536 + + + 480 + + + + + + + &Window Size: + + + windowWidthSpinBox + + + + + + + × + + + + + + + pixels + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + true + + + &Console Window + + + false + + + false + + + + + + When the game is launched, show the console window + + + + + + + When the game crashes, show the console window + + + + + + + When the game quits, hide the console window + + + + + + + + + + &Global Data Packs + + + true + + + true + + + + + + Allows installing data packs across all worlds if an applicable mod is installed. +It is most likely you will need to change the path - please refer to the mod's website. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Folder Path + + + + + + + + + datapacks + + + + + + + Browse + + + + + + + + + + + + true + + + Game &Time + + + false + + + false + + + + + + Show time spent &playing instances + + + + + + + &Record time spent playing instances + + + + + + + Show the &total time played across instances + + + + + + + Always show durations in &hours + + + + + + + + + + Override &Default Account + + + true + + + false + + + + + + Account: + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + Enable Auto-&join + + + true + + + false + + + + + + + 0 + 0 + + + + + + + + Singleplayer world: + + + + + + + Server address: + + + + + + + + 200 + 16777215 + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + Override Mod Download &Loaders + + + true + + + false + + + + + + NeoForge + + + + + + + Forge + + + + + + + Fabric + + + + + + + Quilt + + + + + + + LiteLoader + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Java + + + + + + true + + + + + 0 + 0 + 100 + 30 + + + + + + + + + + Tweaks + + + + + + true + + + + + 0 + 0 + 261 + 434 + + + + + + + &Legacy Tweaks + + + false + + + false + + + + + + <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html> + + + Enable online fixes (experimental) + + + + + + + + + + true + + + &Native Libraries + + + false + + + false + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + &GLFW library path: + + + lineEditGLFWPath + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + &OpenAL library path: + + + lineEditOpenALPath + + + + + + + false + + + + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + + false + + + + + + + + + + true + + + &Performance + + + false + + + false + + + + + + <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> + + + Enable Feral GameMode + + + + + + + <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> + + + Enable MangoHud + + + + + + + <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> + + + Use discrete GPU + + + + + + + Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used. + + + Use Zink + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Custom Commands + + + + + + + + + + Environment Variables + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + CustomCommands + QWidget +
    ui/widgets/CustomCommands.h
    + 1 +
    + + EnvironmentVariables + QWidget +
    ui/widgets/EnvironmentVariables.h
    + 1 +
    +
    + + openGlobalSettingsButton + settingsTabs + scrollArea + maximizedCheckBox + windowHeightSpinBox + windowWidthSpinBox + closeAfterLaunchCheck + quitAfterGameStopCheck + showConsoleCheck + showConsoleErrorCheck + autoCloseConsoleCheck + showGameTime + recordGameTime + showGlobalGameTime + showGameTimeWithoutDays + instanceAccountGroupBox + instanceAccountSelector + serverJoinGroupBox + serverJoinAddressButton + serverJoinAddress + worldJoinButton + worldsCb + javaScrollArea + scrollArea_2 + onlineFixes + useNativeGLFWCheck + lineEditGLFWPath + useNativeOpenALCheck + lineEditOpenALPath + enableFeralGamemodeCheck + enableMangoHud + useDiscreteGpuCheck + useZink + + + +
    diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 68adcdb71..60b298901 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -40,6 +40,7 @@ #include #include #include "BaseVersionList.h" +#include "Json.h" #include "Version.h" #include "meta/Index.h" #include "modplatform/ModIndex.h" @@ -49,9 +50,9 @@ #include "Application.h" #include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) +std::unique_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended) { - return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); + return std::unique_ptr(new ModFilterWidget(instance, extended)); } class VersionBasicModel : public QIdentityProxyModel { @@ -107,13 +108,13 @@ class AllVersionProxyModel : public QSortFilterProxyModel { } }; -ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) - : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) +ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) + : QTabWidget(), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) { ui->setupUi(this); m_versions_proxy = new VersionProxyModel(this); - m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); QAbstractProxyModel* proxy = new VersionBasicModel(this); proxy->setSourceModel(m_versions_proxy); @@ -134,12 +135,13 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi ui->versions->hide(); ui->showAllVersions->hide(); ui->environmentGroup->hide(); + ui->openSource->hide(); } ui->versions->setStyleSheet("combobox-popup: 0;"); ui->version->setStyleSheet("combobox-popup: 0;"); connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged); - connect(ui->versions, QOverload::of(&QComboBox::currentIndexChanged), this, &ModFilterWidget::onVersionFilterChanged); + connect(ui->versions, &QComboBox::currentIndexChanged, this, &ModFilterWidget::onVersionFilterChanged); connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); }); connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged); @@ -147,11 +149,19 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - - connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->legacyFabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->ornithe, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->rift, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked); + + if (!extended) { + ui->showMoreButton->setVisible(false); + ui->extendedModLoadersWidget->setVisible(false); + } if (extended) { connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); @@ -159,6 +169,12 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi } connect(ui->hideInstalled, &QCheckBox::stateChanged, this, &ModFilterWidget::onHideInstalledFilterChanged); + connect(ui->openSource, &QCheckBox::stateChanged, this, &ModFilterWidget::onOpenSourceFilterChanged); + + connect(ui->releaseCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->betaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->alphaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); + connect(ui->unknownCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); setHidden(true); loadVersionList(); @@ -208,14 +224,23 @@ void ModFilterWidget::loadVersionList() void ModFilterWidget::prepareBasicFilter() { + m_filter->openSource = false; if (m_instance) { m_filter->hideInstalled = false; - m_filter->side = ""; // or "both" - auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + m_filter->side = ModPlatform::Side::NoSide; // or "both" + ModPlatform::ModLoaderTypes loaders; + if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool()) { + for (auto loader : Json::toStringList(m_instance->settings()->get("ModDownloadLoaders").toString())) { + loaders |= ModPlatform::getModLoaderFromString(loader); + } + } else { + loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + } ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); ui->forge->setChecked(loaders & ModPlatform::Forge); ui->fabric->setChecked(loaders & ModPlatform::Fabric); ui->quilt->setChecked(loaders & ModPlatform::Quilt); + ui->liteLoader->setChecked(loaders & ModPlatform::LiteLoader); m_filter->loaders = loaders; auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); m_filter->versions.emplace_front(def); @@ -231,7 +256,7 @@ void ModFilterWidget::onShowAllVersionsChanged() if (ui->showAllVersions->isChecked()) m_versions_proxy->clearFilters(); else - m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); } void ModFilterWidget::onVersionFilterChanged(int) @@ -261,6 +286,18 @@ void ModFilterWidget::onLoadersFilterChanged() loaders |= ModPlatform::Fabric; if (ui->quilt->isChecked()) loaders |= ModPlatform::Quilt; + if (ui->liteLoader->isChecked()) + loaders |= ModPlatform::LiteLoader; + if (ui->babric->isChecked()) + loaders |= ModPlatform::Babric; + if (ui->btaBabric->isChecked()) + loaders |= ModPlatform::BTA; + if (ui->legacyFabric->isChecked()) + loaders |= ModPlatform::LegacyFabric; + if (ui->ornithe->isChecked()) + loaders |= ModPlatform::Ornithe; + if (ui->rift->isChecked()) + loaders |= ModPlatform::Rift; m_filter_changed = loaders != m_filter->loaders; m_filter->loaders = loaders; if (m_filter_changed) @@ -269,16 +306,16 @@ void ModFilterWidget::onLoadersFilterChanged() void ModFilterWidget::onSideFilterChanged() { - QString side; - - if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) { - if (ui->clientSide->isChecked()) - side = "client"; - else - side = "server"; + ModPlatform::Side side; + + if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) { + side = ModPlatform::Side::ClientSide; + } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) { + side = ModPlatform::Side::ServerSide; + } else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) { + side = ModPlatform::Side::UniversalSide; } else { - // both are checked or none are checked; in either case no filtering will happen - side = ""; + side = ModPlatform::Side::NoSide; } m_filter_changed = side != m_filter->side; @@ -337,4 +374,36 @@ void ModFilterWidget::setCategories(const QList& categori } } -#include "ModFilterWidget.moc" \ No newline at end of file +void ModFilterWidget::onOpenSourceFilterChanged() +{ + auto open = ui->openSource->isChecked(); + m_filter_changed = open != m_filter->openSource; + m_filter->openSource = open; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onReleaseFilterChanged() +{ + std::list releases; + if (ui->releaseCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType::Release); + if (ui->betaCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType::Beta); + if (ui->alphaCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType::Alpha); + if (ui->unknownCb->isChecked()) + releases.push_back(ModPlatform::IndexedVersionType::Unknown); + m_filter_changed = releases != m_filter->releases; + m_filter->releases = releases; + if (m_filter_changed) + emit filterChanged(); +} + +void ModFilterWidget::onShowMoreClicked() +{ + ui->extendedModLoadersWidget->setVisible(true); + ui->showMoreButton->setVisible(false); +} + +#include "ModFilterWidget.moc" diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 50f0e06c9..f00b98eb0 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -61,14 +61,15 @@ class ModFilterWidget : public QTabWidget { std::list versions; std::list releases; ModPlatform::ModLoaderTypes loaders; - QString side; + ModPlatform::Side side; bool hideInstalled; QStringList categoryIds; + bool openSource; bool operator==(const Filter& other) const { return hideInstalled == other.hideInstalled && side == other.side && loaders == other.loaders && versions == other.versions && - releases == other.releases && categoryIds == other.categoryIds; + releases == other.releases && categoryIds == other.categoryIds && openSource == other.openSource; } bool operator!=(const Filter& other) const { return !(*this == other); } @@ -80,9 +81,17 @@ class ModFilterWidget : public QTabWidget { return versions.empty(); } + + bool checkModpackFilters(const ModPlatform::IndexedVersion& v) + { + return ((!loaders || !v.loaders || loaders & v.loaders) && // loaders + (releases.empty() || // releases + std::find(releases.cbegin(), releases.cend(), v.version_type) != releases.cend()) && + checkMcVersions({ v.mcVersion })); // gameVersion} + } }; - static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + static std::unique_ptr create(MinecraftInstance* instance, bool extended); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; @@ -95,7 +104,7 @@ class ModFilterWidget : public QTabWidget { void setCategories(const QList&); private: - ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport); void loadVersionList(); void prepareBasicFilter(); @@ -107,6 +116,9 @@ class ModFilterWidget : public QTabWidget { void onSideFilterChanged(); void onHideInstalledFilterChanged(); void onShowAllVersionsChanged(); + void onOpenSourceFilterChanged(); + void onReleaseFilterChanged(); + void onShowMoreClicked(); private: Ui::ModFilterWidget* ui; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index 236847094..d29c9752a 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -63,8 +63,8 @@ 0 0 - 308 - 598 + 294 + 781 @@ -121,6 +121,76 @@ + + + + Show More + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + LiteLoader + + + + + + + Babric + + + + + + + BTA (Babric) + + + + + + + Legacy Fabric + + + + + + + Ornithe + + + + + + + Rift + + + + + + @@ -188,6 +258,50 @@ + + + + Open source only + + + + + + + Release type + + + + + + Release + + + + + + + Beta + + + + + + + Alpha + + + + + + + Unknown + + + + + + diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 514e1d25c..58b092275 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -77,6 +77,8 @@ class PageEntryFilterModel : public QSortFilterProxyModel { PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QWidget(parent) { createUI(); + useSidebarStyle(true); + m_model = new PageModel(this); m_proxyModel = new PageEntryFilterModel(this); int counter = 0; @@ -103,7 +105,7 @@ PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); m_pageList->setModel(m_proxyModel); - connect(m_pageList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex))); + connect(m_pageList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &PageContainer::currentChanged); m_pageStack->setStackingMode(QStackedLayout::StackOne); m_pageList->setFocus(); selectPage(defaultId); @@ -160,7 +162,6 @@ void PageContainer::createUI() m_pageStack = new QStackedLayout; m_pageList = new PageView; m_header = new QLabel(); - m_iconHeader = new IconLabel(this, QIcon(), QSize(24, 24)); QFont headerLabelFont = m_header->font(); headerLabelFont.setBold(true); @@ -173,10 +174,6 @@ void PageContainer::createUI() const int leftMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin); headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); headerHLayout->addWidget(m_header); - headerHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored)); - headerHLayout->addWidget(m_iconHeader); - const int rightMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutRightMargin); - headerHLayout->addSpacerItem(new QSpacerItem(rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); headerHLayout->setContentsMargins(0, 6, 0, 0); m_pageStack->setContentsMargins(0, 0, 0, 0); @@ -184,10 +181,10 @@ void PageContainer::createUI() m_layout = new QGridLayout; m_layout->addLayout(headerHLayout, 0, 1, 1, 1); - m_layout->addWidget(m_pageList, 0, 0, 2, 1); + m_layout->addWidget(m_pageList, 0, 0, 3, 1); m_layout->addLayout(m_pageStack, 1, 1, 1, 1); m_layout->setColumnStretch(1, 4); - m_layout->setContentsMargins(0, 0, 0, 6); + m_layout->setContentsMargins(0, 0, 0, 0); setLayout(m_layout); } @@ -202,12 +199,17 @@ void PageContainer::retranslate() void PageContainer::addButtons(QWidget* buttons) { - m_layout->addWidget(buttons, 2, 0, 1, 2); + m_layout->addWidget(buttons, 2, 1, 1, 2); } void PageContainer::addButtons(QLayout* buttons) { - m_layout->addLayout(buttons, 2, 0, 1, 2); + m_layout->addLayout(buttons, 2, 1, 1, 2); +} + +void PageContainer::useSidebarStyle(bool sidebar) +{ + m_pageList->setProperty("_kde_side_panel_view", sidebar); } void PageContainer::showPage(int row) @@ -223,12 +225,10 @@ void PageContainer::showPage(int row) if (m_currentPage) { m_pageStack->setCurrentIndex(m_currentPage->stackIndex); m_header->setText(m_currentPage->displayName()); - m_iconHeader->setIcon(m_currentPage->icon()); m_currentPage->opened(); } else { m_pageStack->setCurrentIndex(0); m_header->setText(QString()); - m_iconHeader->setIcon(APPLICATION->getThemedIcon("bug")); } } diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index ab4444c99..2c7ca9e39 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -61,6 +61,9 @@ class PageContainer : public QWidget, public BasePageContainer { void addButtons(QWidget* buttons); void addButtons(QLayout* buttons); + + void useSidebarStyle(bool sidebar); + /* * Save any unsaved state and prepare to be closed. * @return true if everything can be saved, false if there is something that requires attention @@ -112,6 +115,5 @@ class PageContainer : public QWidget, public BasePageContainer { QStackedLayout* m_pageStack; QListView* m_pageList; QLabel* m_header; - IconLabel* m_iconHeader; QGridLayout* m_layout; }; diff --git a/launcher/ui/widgets/PageContainer_p.h b/launcher/ui/widgets/PageContainer_p.h index e61f6e154..9a7651c75 100644 --- a/launcher/ui/widgets/PageContainer_p.h +++ b/launcher/ui/widgets/PageContainer_p.h @@ -89,8 +89,6 @@ class PageView : public QListView { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); setItemDelegate(new PageViewDelegate(this)); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - // Adjust margins when using Breeze theme - setProperty("_kde_side_panel_view", true); } virtual QSize sizeHint() const diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index 9181de7f8..69c7e6f17 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format) m_bar->setFormat(format); } -void ProgressWidget::watch(const Task* task) +void ProgressWidget::watch(Task* task) { if (!task) return; @@ -61,11 +61,11 @@ void ProgressWidget::watch(const Task* task) connect(m_task, &Task::started, this, &ProgressWidget::show); } -void ProgressWidget::start(const Task* task) +void ProgressWidget::start(Task* task) { watch(task); if (!m_task->isRunning()) - QMetaObject::invokeMethod(const_cast(m_task), "start", Qt::QueuedConnection); + QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); } bool ProgressWidget::exec(std::shared_ptr task) diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index b0458f335..4d9097b8a 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -27,10 +27,10 @@ class ProgressWidget : public QWidget { public slots: /** Watch the progress of a task. */ - void watch(const Task* task); + void watch(Task* task); /** Watch the progress of a task, and start it if needed */ - void start(const Task* task); + void start(Task* task); /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr task); @@ -50,7 +50,7 @@ class ProgressWidget : public QWidget { private: QLabel* m_label = nullptr; QProgressBar* m_bar = nullptr; - const Task* m_task = nullptr; + Task* m_task = nullptr; bool m_hide_if_inactive = false; }; diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 6946df41f..2fd5c97c2 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -1,9 +1,11 @@ #include "ProjectItem.h" -#include "Common.h" +#include +#include #include #include +#include "Common.h" ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} @@ -14,15 +16,36 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QStyleOptionViewItem opt(option); initStyleOption(&opt, index); + auto isInstalled = index.data(UserDataTypes::INSTALLED).toBool(); + auto isChecked = opt.checkState == Qt::Checked; + auto isSelected = option.state & QStyle::State_Selected; + + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + auto rect = opt.rect; - if (opt.state & QStyle::State_Selected) { - painter->fillRect(rect, opt.palette.highlight()); + bool windows = style->objectName().startsWith("windows"); + + if (!windows) + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); + + if (isSelected) { + if (windows) + painter->fillRect(rect, opt.palette.highlight()); + painter->setPen(opt.palette.highlightedText().color()); - } else if (opt.state & QStyle::State_MouseOver) { - painter->fillRect(rect, opt.palette.window()); } + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { + QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); + style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget); + + rect.setX(checkboxOpt.rect.right()); + } + + if (!isSelected && !isChecked && isInstalled) { + 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(); int icon_x_margin = (rect.height() - icon_width) / 2; @@ -42,6 +65,9 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o int x = rect.x() + icon_x_margin; int y = rect.y() + icon_y_margin; + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) + rect.translate(icon_x_margin / 2, 0); + // Prevent 'scaling null pixmap' warnings if (icon_width > 0 && icon_height > 0) opt.icon.paint(painter, x, y, icon_width, icon_height); @@ -59,21 +85,11 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->save(); auto font = opt.font; - if (index.data(UserDataTypes::SELECTED).toBool()) { - // Set nice font + if (isChecked) { font.setBold(true); - font.setUnderline(true); } - if (index.data(UserDataTypes::INSTALLED).toBool()) { - auto hRect = opt.rect; - hRect.setX(hRect.x() + 1); - hRect.setY(hRect.y() + 1); - hRect.setHeight(hRect.height() - 2); - hRect.setWidth(hRect.width() - 2); - // Set nice font - font.setItalic(true); - font.setOverline(true); - painter->drawRect(hRect); + if (isInstalled) { + title = tr("%1 [installed]").arg(title); } font.setPointSize(font.pointSize() + 2); @@ -132,3 +148,56 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->restore(); } + +bool ProjectItemDelegate::editorEvent(QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) +{ + if (!(event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick)) + return false; + + auto mouseEvent = (QMouseEvent*)event; + + if (mouseEvent->button() != Qt::LeftButton) + return false; + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + + const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); + + if (!checkboxOpt.rect.contains(mouseEvent->pos().x(), mouseEvent->pos().y())) + return false; + + // swallow other events + // (prevents item being selected or double click action triggering) + if (event->type() != QEvent::MouseButtonRelease) + return true; + + emit checkboxClicked(index); + return true; +} + +QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const +{ + QStyleOptionViewItem checkboxOpt = opt; + + checkboxOpt.state &= ~QStyle::State_HasFocus; + + if (checkboxOpt.checkState == Qt::Checked) + checkboxOpt.state |= QStyle::State_On; + else + checkboxOpt.state |= QStyle::State_Off; + + QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); + // 5px is the typical top margin for image + // we don't want the checkboxes to be all over the place :) + checkboxOpt.rect = QRect(opt.rect.x() + 5, opt.rect.y() + (opt.rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), + checkboxRect.height()); + + return checkboxOpt; +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index c3d0dce70..068358ade 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -6,8 +6,7 @@ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString - SELECTED = 259, // bool - INSTALLED = 260 // bool + INSTALLED = 259 // bool }; /** This is an item delegate composed of: @@ -22,4 +21,12 @@ class ProjectItemDelegate final : public QStyledItemDelegate { ProjectItemDelegate(QWidget* parent); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + + bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override; + + signals: + void checkboxClicked(const QModelIndex& index); + + private: + QStyleOptionViewItem makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const; }; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp deleted file mode 100644 index 097678b8d..000000000 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2024 Tayou - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include "ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -#include "Application.h" -#include "DesktopServices.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" - -ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) -{ - ui->setupUi(this); - loadSettings(); - ThemeCustomizationWidget::refresh(); - - connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); - - connect(ui->iconsFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); - connect(ui->widgetStyleFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); - connect(ui->catPackFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); - - connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); -} - -ThemeCustomizationWidget::~ThemeCustomizationWidget() -{ - delete ui; -} - -/// -/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead -/// TODO FIXME -/// -/// Original Method One: -/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); -/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); -/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); -/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); -/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); -/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); -/// -/// original Method Two: -/// if (!(features & ThemeFields::ICONS)) { -/// ui->formLayout->setRowVisible(0, false); -/// } -/// if (!(features & ThemeFields::WIDGETS)) { -/// ui->formLayout->setRowVisible(1, false); -/// } -/// if (!(features & ThemeFields::CAT)) { -/// ui->formLayout->setRowVisible(2, false); -/// } -/// -/// -void ThemeCustomizationWidget::showFeatures(ThemeFields features) -{ - ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); - ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); - ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); - ui->widgetStyleLabel->setEnabled(features & ThemeFields::WIDGETS); - ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); - ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); -} - -void ThemeCustomizationWidget::applyIconTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalIconTheme = settings->get("IconTheme").toString(); - auto newIconTheme = ui->iconsComboBox->itemData(index).toString(); - if (originalIconTheme != newIconTheme) { - settings->set("IconTheme", newIconTheme); - APPLICATION->themeManager()->applyCurrentlySelectedTheme(); - } - - emit currentIconThemeChanged(index); -} - -void ThemeCustomizationWidget::applyWidgetTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->itemData(index).toString(); - if (originalAppTheme != newAppTheme) { - settings->set("ApplicationTheme", newAppTheme); - APPLICATION->themeManager()->applyCurrentlySelectedTheme(); - } - - emit currentWidgetThemeChanged(index); -} - -void ThemeCustomizationWidget::applyCatTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalCat = settings->get("BackgroundCat").toString(); - auto newCat = ui->backgroundCatComboBox->itemData(index).toString(); - if (originalCat != newCat) { - settings->set("BackgroundCat", newCat); - } - - emit currentCatChanged(index); -} - -void ThemeCustomizationWidget::applySettings() -{ - applyIconTheme(ui->iconsComboBox->currentIndex()); - applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); - applyCatTheme(ui->backgroundCatComboBox->currentIndex()); -} -void ThemeCustomizationWidget::loadSettings() -{ - auto settings = APPLICATION->settings(); - - { - auto currentIconTheme = settings->get("IconTheme").toString(); - auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); - int idx = 0; - for (auto iconTheme : iconThemes) { - QIcon iconForComboBox = QIcon(iconTheme->path() + "/scalable/settings"); - ui->iconsComboBox->addItem(iconForComboBox, iconTheme->name(), iconTheme->id()); - if (currentIconTheme == iconTheme->id()) { - ui->iconsComboBox->setCurrentIndex(idx); - } - idx++; - } - } - - { - auto currentTheme = settings->get("ApplicationTheme").toString(); - auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); - int idx = 0; - for (auto& theme : themes) { - ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); - if (theme->tooltip() != "") { - int index = ui->widgetStyleComboBox->count() - 1; - ui->widgetStyleComboBox->setItemData(index, theme->tooltip(), Qt::ToolTipRole); - } - if (currentTheme == theme->id()) { - ui->widgetStyleComboBox->setCurrentIndex(idx); - } - idx++; - } - } - - auto cat = settings->get("BackgroundCat").toString(); - for (auto& catFromList : APPLICATION->themeManager()->getValidCatPacks()) { - QIcon catIcon = QIcon(QString("%1").arg(catFromList->path())); - ui->backgroundCatComboBox->addItem(catIcon, catFromList->name(), catFromList->id()); - if (cat == catFromList->id()) { - ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); - } - } -} - -void ThemeCustomizationWidget::retranslate() -{ - ui->retranslateUi(this); -} - -void ThemeCustomizationWidget::refresh() -{ - applySettings(); - disconnect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - disconnect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - disconnect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyCatTheme); - APPLICATION->themeManager()->refresh(); - ui->iconsComboBox->clear(); - ui->widgetStyleComboBox->clear(); - ui->backgroundCatComboBox->clear(); - loadSettings(); - connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); -}; \ No newline at end of file diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui deleted file mode 100644 index 1faa45c4f..000000000 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ /dev/null @@ -1,212 +0,0 @@ - - - ThemeCustomizationWidget - - - - 0 - 0 - 400 - 191 - - - - Form - - - - QLayout::SetMinimumSize - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - &Icons - - - iconsComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View icon themes folder. - - - - - - - - - true - - - - - - - - - &Widgets - - - widgetStyleComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View widget themes folder. - - - - - - - - - true - - - - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - - - - - View cat packs folder. - - - - - - - - - true - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Refresh all - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 2d735d18f..040355f4b 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -224,20 +224,20 @@ BaseVersion::Ptr VersionSelectWidget::selectedVersion() const void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ContainsFilter(filter)); + m_proxyModel->setFilter(role, Filters::contains(filter)); } void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ExactFilter(filter)); + m_proxyModel->setFilter(role, Filters::equals(filter)); } void VersionSelectWidget::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ExactIfPresentFilter(filter)); + m_proxyModel->setFilter(role, Filters::equalsOrEmpty(filter)); } -void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter* filter) +void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter filter) { m_proxyModel->setFilter(role, filter); } diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index c16d4c0dd..c66d7e98e 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -39,13 +39,13 @@ #include #include #include "BaseVersionList.h" +#include "Filter.h" #include "VersionListView.h" class VersionProxyModel; class VersionListView; class QVBoxLayout; class QProgressBar; -class Filter; class VersionSelectWidget : public QWidget { Q_OBJECT @@ -70,7 +70,7 @@ class VersionSelectWidget : public QWidget { void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); - void setFilter(BaseVersionList::ModelRoles role, Filter* filter); + void setFilter(BaseVersionList::ModelRoles role, Filter filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(VersionListView::EmptyMode mode); diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 2940d7ce7..e87c8b4c1 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -250,7 +250,7 @@ void WideBar::addContextMenuAction(QAction* action) m_context_menu_actions.append(action); } -[[nodiscard]] QByteArray WideBar::getVisibilityState() const +QByteArray WideBar::getVisibilityState() const { QByteArray state; diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index f4877a89a..68a052a23 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -35,7 +35,7 @@ class WideBar : public QToolBar { // Ideally we would use a QBitArray for this, but it doesn't support string conversion, // so using it in settings is very messy. - [[nodiscard]] QByteArray getVisibilityState() const; + QByteArray getVisibilityState() const; void setVisibilityState(QByteArray&&); void removeAction(QAction* action); @@ -50,8 +50,8 @@ class WideBar : public QToolBar { auto getMatching(QAction* act) -> QList::iterator; /** Used to distinguish between versions of the WideBar with different actions */ - [[nodiscard]] QByteArray getHash() const; - [[nodiscard]] bool checkHash(QByteArray const&) const; + QByteArray getHash() const; + bool checkHash(QByteArray const&) const; private: QList m_entries; diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index b2b631593..c54708ee1 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -166,7 +166,7 @@ @implementation UpdaterDelegate QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; - foreach (const QString channel, channels) { + for (const QString& channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 8bf8cb473..bffc6630c 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -46,31 +46,12 @@ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include -#include -#include #include -#include +#include "console/WindowsConsole.h" #endif -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif #include "DesktopServices.h" @@ -103,112 +84,12 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } } -#if defined Q_OS_WIN32 - -// taken from https://stackoverflow.com/a/25927081 -// getting a proper output to console with redirection support on windows is apparently hell -void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) -{ - // Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been - // observed that the file number of our standard handle file objects can be assigned internally to a value of -2 - // when not bound to a valid target, which represents some kind of unknown internal invalid state. In this state our - // call to "_dup2" fails, as it specifically tests to ensure that the target file number isn't equal to this value - // before allowing the operation to continue. We can resolve this issue by first "re-opening" the target files to - // use the "nul" device, which will place them into a valid state, after which we can redirect them to our target - // using the "_dup2" function. - if (bindStdIn) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "r", stdin); - } - if (bindStdOut) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stdout); - } - if (bindStdErr) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stderr); - } - - // Redirect unbuffered stdin from the current standard input handle - if (bindStdIn) { - HANDLE stdHandle = GetStdHandle(STD_INPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "r"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdin)); - if (dup2Result == 0) { - setvbuf(stdin, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stdout to the current standard output handle - if (bindStdOut) { - HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdout)); - if (dup2Result == 0) { - setvbuf(stdout, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stderr to the current standard error handle - if (bindStdErr) { - HANDLE stdHandle = GetStdHandle(STD_ERROR_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stderr)); - if (dup2Result == 0) { - setvbuf(stderr, NULL, _IONBF, 0); - } - } - } - } - } - - // Clear the error state for each of the C++ standard stream objects. We need to do this, as attempts to access the - // standard streams before they refer to a valid target will cause the iostream objects to enter an error state. In - // versions of Visual Studio after 2005, this seems to always occur during startup regardless of whether anything - // has been read from or written to the targets or not. - if (bindStdIn) { - std::wcin.clear(); - std::cin.clear(); - } - if (bindStdOut) { - std::wcout.clear(); - std::cout.clear(); - } - if (bindStdErr) { - std::wcerr.clear(); - std::cerr.clear(); - } -} -#endif - PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured - auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); - if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) { - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - BindCrtHandlesToStdHandles(true, true, true); - consoleAttached = true; - } + if (AttachWindowsConsole()) { + consoleAttached = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); @@ -242,66 +123,6 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar logToConsole = parser.isSet("debug"); - auto updater_executable = QCoreApplication::applicationFilePath(); - -#ifdef Q_OS_MACOS - showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); -#endif - - if (updater_executable.startsWith("/tmp/.mount_")) { - m_isAppimage = true; - m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (m_appimagePath.isEmpty()) { - showFatalErrorMessage(tr("Unsupported Installation"), - tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } - } - - m_isFlatpak = DesktopServices::isFlatpak(); - - QString prism_executable = FS::PathCombine(applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME); -#if defined Q_OS_WIN32 - prism_executable.append(".exe"); -#endif - - if (!QFileInfo(prism_executable).isFile()) { - showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); - } - - m_prismExecutable = prism_executable; - - auto prism_update_url = parser.value("update-url"); - if (prism_update_url.isEmpty()) - prism_update_url = BuildConfig.UPDATER_GITHUB_REPO; - - m_prismRepoUrl = QUrl::fromUserInput(prism_update_url); - - m_checkOnly = parser.isSet("check-only"); - m_forceUpdate = parser.isSet("force"); - m_printOnly = parser.isSet("list"); - auto user_version = parser.value("install-version"); - if (!user_version.isEmpty()) { - m_userSelectedVersion = Version(user_version); - } - m_selectUI = parser.isSet("select-ui"); - m_allowDowngrade = parser.isSet("allow-downgrade"); - - auto version = parser.value("prism-version"); - if (!version.isEmpty()) { - if (version.contains('-')) { - auto index = version.indexOf('-'); - m_prsimVersionChannel = version.mid(index + 1); - version = version.left(index); - } else { - m_prsimVersionChannel = "stable"; - } - auto version_parts = version.split('.'); - m_prismVersionMajor = version_parts.takeFirst().toInt(); - m_prismVersionMinor = version_parts.takeFirst().toInt(); - } - - m_allowPreRelease = parser.isSet("pre-release"); - QString origCwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); @@ -472,12 +293,74 @@ 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); } +#ifdef Q_OS_MACOS + showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); +#endif + + if (binPath.startsWith("/tmp/.mount_")) { + m_isAppimage = true; + m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (m_appimagePath.isEmpty()) { + showFatalErrorMessage(tr("Unsupported Installation"), + tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + } + + m_isFlatpak = DesktopServices::isFlatpak(); + + QString prism_executable = FS::PathCombine(binPath, BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + prism_executable.append(".exe"); +#endif + + if (!QFileInfo(prism_executable).isFile()) { + showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); + } + + m_prismExecutable = prism_executable; + + auto prism_update_url = parser.value("update-url"); + if (prism_update_url.isEmpty()) + prism_update_url = BuildConfig.UPDATER_GITHUB_REPO; + + m_prismRepoUrl = QUrl::fromUserInput(prism_update_url); + + m_checkOnly = parser.isSet("check-only"); + m_forceUpdate = parser.isSet("force"); + m_printOnly = parser.isSet("list"); + auto user_version = parser.value("install-version"); + if (!user_version.isEmpty()) { + m_userSelectedVersion = Version(user_version); + } + m_selectUI = parser.isSet("select-ui"); + m_allowDowngrade = parser.isSet("allow-downgrade"); + + auto version = parser.value("prism-version"); + if (!version.isEmpty()) { + if (version.contains('-')) { + auto index = version.indexOf('-'); + m_prsimVersionChannel = version.mid(index + 1); + version = version.left(index); + } else { + m_prsimVersionChannel = "stable"; + } + auto version_parts = version.split('.'); + m_prismVersionMajor = version_parts.takeFirst().toInt(); + m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; + } + + m_allowPreRelease = parser.isSet("pre-release"); + auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker"); auto marker_file = QFileInfo(marker_file_path); if (marker_file.exists()) { @@ -556,6 +439,7 @@ void PrismUpdaterApp::run() m_prismVersion = BuildConfig.printableVersionString(); m_prismVersionMajor = BuildConfig.VERSION_MAJOR; m_prismVersionMinor = BuildConfig.VERSION_MINOR; + m_prismVersionPatch = BuildConfig.VERSION_PATCH; m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; m_prismGitCommit = BuildConfig.GIT_COMMIT; } @@ -564,6 +448,7 @@ void PrismUpdaterApp::run() qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; qDebug() << "Version major:" << m_prismVersionMajor; qDebug() << "Version minor:" << m_prismVersionMinor; + qDebug() << "Version minor:" << m_prismVersionPatch; qDebug() << "Version channel:" << m_prsimVersionChannel; qDebug() << "Git Commit:" << m_prismGitCommit; @@ -834,8 +719,8 @@ QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRel for_platform = false; } - auto qt_pattern = QRegularExpression("-qt(\\d+)"); - auto qt_match = qt_pattern.match(asset_name); + static const QRegularExpression s_qtPattern("-qt(\\d+)"); + auto qt_match = s_qtPattern.match(asset_name); if (for_platform && qt_match.hasMatch()) { if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" @@ -907,7 +792,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(); @@ -924,7 +809,7 @@ bool PrismUpdaterApp::callAppImageUpdate() auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); QProcess proc = QProcess(); qDebug() << "Calling: AppImageUpdate" << appimage_path; - proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate-x86_64.AppImage")); + proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage")); proc.setArguments({ appimage_path }); auto result = proc.startDetached(); if (!result) @@ -1131,12 +1016,11 @@ void PrismUpdaterApp::backupAppDir() logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); + static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); auto app_dir = QDir(m_rootPath); - auto backup_dir = FS::PathCombine( - app_dir.absolutePath(), - QStringLiteral("backup_") + - QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) + "-" + - m_prismGitCommit); + auto backup_dir = + FS::PathCombine(app_dir.absolutePath(), + QStringLiteral("backup_") + QString(m_prismVersion).replace(s_replaceRegex, QString("_")) + "-" + m_prismGitCommit); FS::ensureFolderPathExists(backup_dir); auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); FS::write(backup_marker_path, backup_dir.toUtf8()); @@ -1192,42 +1076,13 @@ std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) FS::ensureFolderPathExists(temp_extract_path); auto tmp_extract_dir = QDir(temp_extract_path); - if (archive.fileName().endsWith(".zip")) { - auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); - if (result) { - logUpdate(tr("Extracted the following to \"%1\":\n %2").arg(tmp_extract_dir.absolutePath()).arg(result->join("\n "))); - } else { - logUpdate(tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); - showFatalErrorMessage("Failed to extract archive", - tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); - return std::nullopt; - } - - } else if (archive.fileName().endsWith(".tar.gz")) { - QString cmd = "tar"; - QStringList args = { "-xvf", archive.absoluteFilePath(), "-C", tmp_extract_dir.absolutePath() }; - logUpdate(tr("Running: `%1 %2`").arg(cmd).arg(args.join(" "))); - QProcess proc = QProcess(); - proc.start(cmd, args); - if (!proc.waitForStarted(5000)) { // wait 5 seconds to start - auto msg = tr("Failed to launch child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); - logUpdate(msg); - showFatalErrorMessage(tr("Failed extract archive"), msg); - return std::nullopt; - } - auto result = proc.waitForFinished(5000); - auto out = proc.readAll(); - logUpdate(out); - if (!result) { - auto msg = tr("Child process \"%1 %2\" failed.").arg(cmd).arg(args.join(" ")); - logUpdate(msg); - showFatalErrorMessage(tr("Failed to extract archive"), msg); - return std::nullopt; - } - + auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); + if (result) { + logUpdate(tr("Extracted the following to \"%1\":\n %2").arg(tmp_extract_dir.absolutePath()).arg(result->join("\n "))); } else { - logUpdate(tr("Unknown archive format for %1").arg(archive.absoluteFilePath())); - showFatalErrorMessage("Can not extract", QStringLiteral("Unknown archive format %1").arg(archive.absoluteFilePath())); + logUpdate(tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); + showFatalErrorMessage("Failed to extract archive", + tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); return std::nullopt; } @@ -1277,6 +1132,10 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) return false; m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; m_prismGitCommit = lines.takeFirst().simplified(); return true; } @@ -1303,16 +1162,16 @@ 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 = Net::Download::makeByteArray(page_url, response.get()); + 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()); @@ -1349,13 +1208,13 @@ int PrismUpdaterApp::parseReleasePage(const QByteArray* response) GitHubRelease release = {}; release.id = Json::requireInteger(release_obj, "id"); - release.name = Json::ensureString(release_obj, "name"); + release.name = release_obj["name"].toString(); release.tag_name = Json::requireString(release_obj, "tag_name"); release.created_at = QDateTime::fromString(Json::requireString(release_obj, "created_at"), Qt::ISODate); - release.published_at = QDateTime::fromString(Json::ensureString(release_obj, "published_at"), Qt::ISODate); + release.published_at = QDateTime::fromString(release_obj["published_at"].toString(), Qt::ISODate); release.draft = Json::requireBoolean(release_obj, "draft"); release.prerelease = Json::requireBoolean(release_obj, "prerelease"); - release.body = Json::ensureString(release_obj, "body"); + release.body = release_obj["body"].toString(); release.version = Version(release.tag_name); auto release_assets_obj = Json::requireArray(release_obj, "assets"); @@ -1364,7 +1223,7 @@ int PrismUpdaterApp::parseReleasePage(const QByteArray* response) GitHubReleaseAsset asset = {}; asset.id = Json::requireInteger(asset_obj, "id"); asset.name = Json::requireString(asset_obj, "name"); - asset.label = Json::ensureString(asset_obj, "label"); + asset.label = asset_obj["label"].toString(); asset.content_type = Json::requireString(asset_obj, "content_type"); asset.size = Json::requireInteger(asset_obj, "size"); asset.created_at = QDateTime::fromString(Json::requireString(asset_obj, "created_at"), Qt::ISODate); @@ -1400,7 +1259,7 @@ GitHubRelease PrismUpdaterApp::getLatestRelease() bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) { - auto current_ver = Version(QString("%1.%2").arg(QString::number(m_prismVersionMajor)).arg(QString::number(m_prismVersionMinor))); + auto current_ver = Version(QString("%1.%2.%3").arg(m_prismVersionMajor).arg(m_prismVersionMinor).arg(m_prismVersionPatch)); return current_ver < release.version; } diff --git a/launcher/updater/prismupdater/PrismUpdater.h b/launcher/updater/prismupdater/PrismUpdater.h index f3dd6e062..f141b6c7d 100644 --- a/launcher/updater/prismupdater/PrismUpdater.h +++ b/launcher/updater/prismupdater/PrismUpdater.h @@ -121,13 +121,14 @@ class PrismUpdaterApp : public QApplication { QString m_prismVersion; int m_prismVersionMajor = -1; int m_prismVersionMinor = -1; + int m_prismVersionPatch = -1; QString m_prsimVersionChannel; QString m_prismGitCommit; 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/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt index b736cefcb..539cea135 100644 --- a/libraries/LocalPeer/CMakeLists.txt +++ b/libraries/LocalPeer/CMakeLists.txt @@ -1,11 +1,8 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(LocalPeer) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Network REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) - find_package(Qt6 COMPONENTS Core Network Core5Compat REQUIRED) - list(APPEND LocalPeer_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) + find_package(Qt6 COMPONENTS Core Network REQUIRED) endif() set(SINGLE_SOURCES diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index bd407042f..7c97579fc 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -72,11 +72,12 @@ ApplicationId ApplicationId::fromTraditionalApp() protoId = protoId.toLower(); #endif auto prefix = protoId.section(QLatin1Char('/'), -1); - prefix.remove(QRegularExpression("[^a-zA-Z]")); + static const QRegularExpression s_removeChars("[^a-zA-Z]"); + prefix.remove(s_removeChars); prefix.truncate(6); QByteArray idc = protoId.toUtf8(); - quint16 idNum = qChecksum(idc.constData(), idc.size()); - auto socketName = QLatin1String("qtsingleapp-") + prefix + QLatin1Char('-') + QString::number(idNum, 16); + quint16 idNum = qChecksum(idc); + auto socketName = QLatin1String("pl") + prefix + QLatin1Char('-') + QString::number(idNum, 16).left(12); #if defined(Q_OS_WIN) if (!pProcessIdToSessionId) { QLibrary lib("kernel32"); @@ -98,12 +99,12 @@ ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const Q QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); QString result = dataPath + QLatin1Char('-') + version; shasum.addData(result.toUtf8()); - return ApplicationId(QLatin1String("qtsingleapp-") + QString::fromLatin1(shasum.result().toHex())); + return ApplicationId(QLatin1String("pl") + QString::fromLatin1(shasum.result().toHex()).left(12)); } ApplicationId ApplicationId::fromCustomId(const QString& id) { - return ApplicationId(QLatin1String("qtsingleapp-") + id); + return ApplicationId(QLatin1String("pl") + id); } ApplicationId ApplicationId::fromRawString(const QString& id) @@ -139,13 +140,13 @@ bool LocalPeer::isClient() #if defined(Q_OS_UNIX) // ### Workaround if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { - QFile::remove(QDir::cleanPath(QDir::tempPath()) + QLatin1Char('/') + socketName); + QLocalServer::removeServer(socketName); res = server->listen(socketName); } #endif if (!res) qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); - QObject::connect(server.get(), SIGNAL(newConnection()), SLOT(receiveConnection())); + connect(server.get(), &QLocalServer::newConnection, this, &LocalPeer::receiveConnection); return false; } diff --git a/libraries/LocalPeer/src/LockedFile.h b/libraries/LocalPeer/src/LockedFile.h index e8023251c..0d3539708 100644 --- a/libraries/LocalPeer/src/LockedFile.h +++ b/libraries/LocalPeer/src/LockedFile.h @@ -42,7 +42,7 @@ #include #ifdef Q_OS_WIN -#include +#include #endif class LockedFile : public QFile { @@ -64,7 +64,7 @@ class LockedFile : public QFile { #ifdef Q_OS_WIN Qt::HANDLE wmutex; Qt::HANDLE rmutex; - QVector rmutexes; + QList rmutexes; QString mutexname; Qt::HANDLE getMutexHandle(int idx, bool doCreate); diff --git a/libraries/README.md b/libraries/README.md index 67d78dade..df4d251f7 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -2,30 +2,6 @@ This folder has third-party or otherwise external libraries needed for other parts to work. -## filesystem - -Gulrak's implementation of C++17 std::filesystem for C++11 /C++14/C++17/C++20 on Windows, macOS, Linux and FreeBSD. - -See [github repo](https://github.com/gulrak/filesystem). - -MIT licensed. - -## gamemode - -A performance optimization daemon. - -See [github repo](https://github.com/FeralInteractive/gamemode). - -BSD-3-Clause licensed - -## cmark - -The C reference implementation of CommonMark, a standardized Markdown spec. - -See [github_repo](https://github.com/commonmark/cmark). - -BSD2 licensed. - ## javacheck Simple Java tool that prints the JVM details - version and platform bitness. @@ -107,12 +83,6 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). -## quazip - -A zip manipulation library. - -LGPL 2.1 with linking exception. - ## rainbow Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring. @@ -125,14 +95,6 @@ A Prism Launcher-specific library for probing system information. Apache 2.0 -## tomlplusplus - -A TOML language parser. Used by Forge 1.14+ to store mod metadata. - -See [github repo](https://github.com/marzer/tomlplusplus). - -Licenced under the MIT licence. - ## qdcss A quick and dirty css parser, used by NilLoader to store mod metadata. diff --git a/libraries/cmark b/libraries/cmark deleted file mode 160000 index 3460cd809..000000000 --- a/libraries/cmark +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3460cd809b6dd311b58e92733ece2fc956224fd2 diff --git a/libraries/extra-cmake-modules b/libraries/extra-cmake-modules deleted file mode 160000 index a3d9394ab..000000000 --- a/libraries/extra-cmake-modules +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a3d9394aba4b35789293378e04fb7473d65edf97 diff --git a/libraries/filesystem b/libraries/filesystem deleted file mode 160000 index 076592ce6..000000000 --- a/libraries/filesystem +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 076592ce6e64568521b88a11881aa36b3d3f7048 diff --git a/libraries/gamemode/CMakeLists.txt b/libraries/gamemode/CMakeLists.txt deleted file mode 100644 index 9e07f34ac..000000000 --- a/libraries/gamemode/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.9.4) -project(gamemode - VERSION 1.6.1) - -add_library(gamemode) -target_include_directories(gamemode PUBLIC include) -target_link_libraries(gamemode PUBLIC ${CMAKE_DL_LIBS}) diff --git a/libraries/gamemode/include/gamemode_client.h b/libraries/gamemode/include/gamemode_client.h deleted file mode 100644 index b186cd489..000000000 --- a/libraries/gamemode/include/gamemode_client.h +++ /dev/null @@ -1,337 +0,0 @@ -/* - -Copyright (c) 2017-2019, Feral Interactive -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Feral Interactive nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - */ -#ifndef CLIENT_GAMEMODE_H -#define CLIENT_GAMEMODE_H -/* - * GameMode supports the following client functions - * Requests are refcounted in the daemon - * - * int gamemode_request_start() - Request gamemode starts - * 0 if the request was sent successfully - * -1 if the request failed - * - * int gamemode_request_end() - Request gamemode ends - * 0 if the request was sent successfully - * -1 if the request failed - * - * GAMEMODE_AUTO can be defined to make the above two functions apply during static init and - * destruction, as appropriate. In this configuration, errors will be printed to stderr - * - * int gamemode_query_status() - Query the current status of gamemode - * 0 if gamemode is inactive - * 1 if gamemode is active - * 2 if gamemode is active and this client is registered - * -1 if the query failed - * - * int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process - * 0 if the request was sent successfully - * -1 if the request failed - * -2 if the request was rejected - * - * int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process - * 0 if the request was sent successfully - * -1 if the request failed - * -2 if the request was rejected - * - * int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process - * 0 if gamemode is inactive - * 1 if gamemode is active - * 2 if gamemode is active and this client is registered - * -1 if the query failed - * - * const char* gamemode_error_string() - Get an error string - * returns a string describing any of the above errors - * - * Note: All the above requests can be blocking - dbus requests can and will block while the daemon - * handles the request. It is not recommended to make these calls in performance critical code - */ - -#include -#include - -#include -#include - -#include - -static char internal_gamemode_client_error_string[512] = { 0 }; - -/** - * Load libgamemode dynamically to dislodge us from most dependencies. - * This allows clients to link and/or use this regardless of runtime. - * See SDL2 for an example of the reasoning behind this in terms of - * dynamic versioning as well. - */ -static volatile int internal_libgamemode_loaded = 1; - -/* Typedefs for the functions to load */ -typedef int (*api_call_return_int)(void); -typedef const char* (*api_call_return_cstring)(void); -typedef int (*api_call_pid_return_int)(pid_t); - -/* Storage for functors */ -static api_call_return_int REAL_internal_gamemode_request_start = NULL; -static api_call_return_int REAL_internal_gamemode_request_end = NULL; -static api_call_return_int REAL_internal_gamemode_query_status = NULL; -static api_call_return_cstring REAL_internal_gamemode_error_string = NULL; -static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL; -static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL; -static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL; - -/** - * Internal helper to perform the symbol binding safely. - * - * Returns 0 on success and -1 on failure - */ -__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(void* handle, - const char* name, - void** out_func, - size_t func_size, - bool required) -{ - void* symbol_lookup = NULL; - char* dl_error = NULL; - - /* Safely look up the symbol */ - symbol_lookup = dlsym(handle, name); - dl_error = dlerror(); - if (required && (dl_error || !symbol_lookup)) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), "dlsym failed - %s", dl_error); - return -1; - } - - /* Have the symbol correctly, copy it to make it usable */ - memcpy(out_func, &symbol_lookup, func_size); - return 0; -} - -/** - * Loads libgamemode and needed functions - * - * Returns 0 on success and -1 on failure - */ -__attribute__((always_inline)) static inline int internal_load_libgamemode(void) -{ - /* We start at 1, 0 is a success and -1 is a fail */ - if (internal_libgamemode_loaded != 1) { - return internal_libgamemode_loaded; - } - - /* Anonymous struct type to define our bindings */ - struct binding { - const char* name; - void** functor; - size_t func_size; - bool required; - } bindings[] = { - { "real_gamemode_request_start", (void**)&REAL_internal_gamemode_request_start, sizeof(REAL_internal_gamemode_request_start), - true }, - { "real_gamemode_request_end", (void**)&REAL_internal_gamemode_request_end, sizeof(REAL_internal_gamemode_request_end), true }, - { "real_gamemode_query_status", (void**)&REAL_internal_gamemode_query_status, sizeof(REAL_internal_gamemode_query_status), false }, - { "real_gamemode_error_string", (void**)&REAL_internal_gamemode_error_string, sizeof(REAL_internal_gamemode_error_string), true }, - { "real_gamemode_request_start_for", (void**)&REAL_internal_gamemode_request_start_for, - sizeof(REAL_internal_gamemode_request_start_for), false }, - { "real_gamemode_request_end_for", (void**)&REAL_internal_gamemode_request_end_for, sizeof(REAL_internal_gamemode_request_end_for), - false }, - { "real_gamemode_query_status_for", (void**)&REAL_internal_gamemode_query_status_for, - sizeof(REAL_internal_gamemode_query_status_for), false }, - }; - - void* libgamemode = NULL; - - /* Try and load libgamemode */ - libgamemode = dlopen("libgamemode.so.0", RTLD_NOW); - if (!libgamemode) { - /* Attempt to load unversioned library for compatibility with older - * versions (as of writing, there are no ABI changes between the two - - * this may need to change if ever ABI-breaking changes are made) */ - libgamemode = dlopen("libgamemode.so", RTLD_NOW); - if (!libgamemode) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), "dlopen failed - %s", dlerror()); - internal_libgamemode_loaded = -1; - return -1; - } - } - - /* Attempt to bind all symbols */ - for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { - struct binding* binder = &bindings[i]; - - if (internal_bind_libgamemode_symbol(libgamemode, binder->name, binder->functor, binder->func_size, binder->required)) { - internal_libgamemode_loaded = -1; - return -1; - }; - } - - /* Success */ - internal_libgamemode_loaded = 0; - return 0; -} - -/** - * Redirect to the real libgamemode - */ -__attribute__((always_inline)) static inline const char* gamemode_error_string(void) -{ - /* If we fail to load the system gamemode, or we have an error string already, return our error - * string instead of diverting to the system version */ - if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') { - return internal_gamemode_client_error_string; - } - - return REAL_internal_gamemode_error_string(); -} - -/** - * Redirect to the real libgamemode - * Allow automatically requesting game mode - * Also prints errors as they happen. - */ -#ifdef GAMEMODE_AUTO -__attribute__((constructor)) -#else -__attribute__((always_inline)) static inline -#endif -int gamemode_request_start(void) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { -#ifdef GAMEMODE_AUTO - fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); -#endif - return -1; - } - - if (REAL_internal_gamemode_request_start() < 0) { -#ifdef GAMEMODE_AUTO - fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); -#endif - return -1; - } - - return 0; -} - -/* Redirect to the real libgamemode */ -#ifdef GAMEMODE_AUTO -__attribute__((destructor)) -#else -__attribute__((always_inline)) static inline -#endif -int gamemode_request_end(void) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { -#ifdef GAMEMODE_AUTO - fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); -#endif - return -1; - } - - if (REAL_internal_gamemode_request_end() < 0) { -#ifdef GAMEMODE_AUTO - fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string()); -#endif - return -1; - } - - return 0; -} - -/* Redirect to the real libgamemode */ -__attribute__((always_inline)) static inline int gamemode_query_status(void) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { - return -1; - } - - if (REAL_internal_gamemode_query_status == NULL) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), - "gamemode_query_status missing (older host?)"); - return -1; - } - - return REAL_internal_gamemode_query_status(); -} - -/* Redirect to the real libgamemode */ -__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { - return -1; - } - - if (REAL_internal_gamemode_request_start_for == NULL) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), - "gamemode_request_start_for missing (older host?)"); - return -1; - } - - return REAL_internal_gamemode_request_start_for(pid); -} - -/* Redirect to the real libgamemode */ -__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { - return -1; - } - - if (REAL_internal_gamemode_request_end_for == NULL) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), - "gamemode_request_end_for missing (older host?)"); - return -1; - } - - return REAL_internal_gamemode_request_end_for(pid); -} - -/* Redirect to the real libgamemode */ -__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid) -{ - /* Need to load gamemode */ - if (internal_load_libgamemode() < 0) { - return -1; - } - - if (REAL_internal_gamemode_query_status_for == NULL) { - snprintf(internal_gamemode_client_error_string, sizeof(internal_gamemode_client_error_string), - "gamemode_query_status_for missing (older host?)"); - return -1; - } - - return REAL_internal_gamemode_query_status_for(pid); -} - -#endif // CLIENT_GAMEMODE_H diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index 7fb05b423..b9bcb121a 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,10 +1,10 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) -find_package(Java 1.8 REQUIRED COMPONENTS Development) +find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck) -set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) set(SRC JavaCheck.java diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 8c5ac342a..dfc4ebb32 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,10 +1,10 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) -find_package(Java 1.8 REQUIRED COMPONENTS Development) +find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.prismlauncher.EntryPoint) -set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7) set(SRC org/prismlauncher/EntryPoint.java diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java index 41f7f9114..34313e91a 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java @@ -49,7 +49,7 @@ @SuppressWarnings("unchecked") public final class MojangApi { public static String getUuid(String username) throws IOException { - try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + username).openStream()) { + try (InputStream in = new URL("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + username).openStream()) { Map map = (Map) JsonParser.parse(in); return (String) map.get("id"); } diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index dc518be64..968499ff6 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -76,13 +76,10 @@ public StandardLauncher(Parameters params) { @Override public void launch() throws Throwable { // window size, title and state - // FIXME doesn't support maximisation - if (!maximize) { - gameArgs.add("--width"); - gameArgs.add(Integer.toString(width)); - gameArgs.add("--height"); - gameArgs.add(Integer.toString(height)); - } + gameArgs.add("--width"); + gameArgs.add(Integer.toString(width)); + gameArgs.add("--height"); + gameArgs.add(Integer.toString(height)); if (serverAddress != null) { if (quickPlayMultiplayerSupported) { @@ -100,6 +97,19 @@ public void launch() throws Throwable { gameArgs.add(worldName); } + StringBuilder joinedGameArgs = new StringBuilder(); + for (String gameArg : gameArgs) { + if (joinedGameArgs.length() > 0) { + joinedGameArgs.append('\u001F'); // unit separator, designed for this purpose + } + joinedGameArgs.append(gameArg); + } + + // pass the real main class and game arguments in so mods can access them + System.setProperty("org.prismlauncher.launch.mainclass", mainClassName); + // unit separator ('\u001F') delimited list of game args + System.setProperty("org.prismlauncher.launch.gameargs", joinedGameArgs.toString()); + // find and invoke the main method MethodHandle method = ReflectionUtils.findMainMethod(mainClassName); method.invokeExact(gameArgs.toArray(new String[0])); diff --git a/libraries/murmur2/CMakeLists.txt b/libraries/murmur2/CMakeLists.txt index f3068201d..be989ee36 100644 --- a/libraries/murmur2/CMakeLists.txt +++ b/libraries/murmur2/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(murmur2) set(MURMUR_SOURCES diff --git a/libraries/qdcss/CMakeLists.txt b/libraries/qdcss/CMakeLists.txt index 0afdef321..d1c1078cd 100644 --- a/libraries/qdcss/CMakeLists.txt +++ b/libraries/qdcss/CMakeLists.txt @@ -1,11 +1,8 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(qdcss) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) - find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED) - list(APPEND qdcss_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) + find_package(Qt6 COMPONENTS Core REQUIRED) endif() set(QDCSS_SOURCES diff --git a/libraries/qdcss/src/qdcss.cpp b/libraries/qdcss/src/qdcss.cpp index c531fb63d..bf0ef63cb 100644 --- a/libraries/qdcss/src/qdcss.cpp +++ b/libraries/qdcss/src/qdcss.cpp @@ -8,19 +8,19 @@ #include #include -QRegularExpression ruleset_re = QRegularExpression(R"([#.]?(@?\w+?)\s*\{(.*?)\})", QRegularExpression::DotMatchesEverythingOption); -QRegularExpression rule_re = QRegularExpression(R"((\S+?)\s*:\s*(?:\"(.*?)(? +static const QRegularExpression s_distoSplitRegex("\\s+"); + Sys::DistributionInfo Sys::read_os_release() { Sys::DistributionInfo out; @@ -145,11 +147,7 @@ void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) vers = lsb.codename; } else { // ubuntu, debian, gentoo, scientific, slackware, ... ? -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto parts = dist.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - auto parts = dist.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + auto parts = dist.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (parts.size()) { dist = parts[0]; } @@ -182,11 +180,7 @@ QString Sys::_extract_distribution(const QString& x) if (release.startsWith("suse linux enterprise")) { return "sles"; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList list = release.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = release.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + QStringList list = release.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (list.size()) { return list[0]; } @@ -195,15 +189,11 @@ QString Sys::_extract_distribution(const QString& x) QString Sys::_extract_version(const QString& x) { - QRegularExpression versionish_string(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - QStringList list = x.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = x.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif + static const QRegularExpression s_versionishString(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); + QStringList list = x.split(s_distoSplitRegex, Qt::SkipEmptyParts); for (int i = list.size() - 1; i >= 0; --i) { QString chunk = list[i]; - if (versionish_string.match(chunk).hasMatch()) { + if (s_versionishString.match(chunk).hasMatch()) { return chunk; } } diff --git a/libraries/tomlplusplus b/libraries/tomlplusplus deleted file mode 160000 index c4369ae1d..000000000 --- a/libraries/tomlplusplus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c4369ae1d8955cae20c4ab40b9813ef4b60e48be diff --git a/libraries/zlib b/libraries/zlib deleted file mode 160000 index 51b7f2abd..000000000 --- a/libraries/zlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 27ad9fdf7..935f1faf0 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -14,21 +14,25 @@ set(Launcher_DisplayName "NMC Launcher") set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE) set(Launcher_DisplayName "${Launcher_DisplayName}" PARENT_SCOPE) -set(Launcher_Copyright "© 2025 sogik\\n© 2022-2025 Prism Launcher Contributors\\n© 2022-2023 PollyMC Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") -set(Launcher_Copyright_Mac "© 2025 sogik, © 2022-2025 Prism Launcher Contributors, © 2022-2023 PollyMC Contributors, © 2021-2022 PolyMC Contributors and © 2012-2021 MultiMC Contributors" PARENT_SCOPE) +set(Launcher_AppID "org.sogik.NMCLauncher") +set(Launcher_SVGFileName "${Launcher_AppID}.svg") +set(Launcher_Copyright "© 2026 sogik\\n© 2022-2026 Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") +set(Launcher_Copyright_Mac "© 2026 sogik\\n© 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 "" PARENT_SCOPE) -set(Launcher_UserAgent "PrismLauncher/${Launcher_VERSION_NAME}" PARENT_SCOPE) +set(Launcher_Domain "https://nmclauncher.nartics.xyz" PARENT_SCOPE) +set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_VERSION_NAME}" PARENT_SCOPE) set(Launcher_ConfigFile "nmclauncher.cfg" PARENT_SCOPE) set(Launcher_Git "https://github.com/sogik/NMCLauncher" PARENT_SCOPE) -set(Launcher_DesktopFileName "org.sogik.NMCLauncher.desktop" PARENT_SCOPE) -set(Launcher_SVGFileName "org.sogik.NMCLauncher.svg" PARENT_SCOPE) +set(Launcher_AppID "${Launcher_AppID}" PARENT_SCOPE) +set(Launcher_SVGFileName "${Launcher_SVGFileName}" PARENT_SCOPE) -set(Launcher_Desktop "program_info/org.sogik.NMCLauncher.desktop" PARENT_SCOPE) +set(Launcher_Desktop "program_info/${Launcher_AppID}.desktop" PARENT_SCOPE) set(Launcher_mrpack_MIMEInfo "program_info/modrinth-mrpack-mime.xml" PARENT_SCOPE) -set(Launcher_MetaInfo "program_info/org.sogik.NMCLauncher.metainfo.xml" PARENT_SCOPE) -set(Launcher_SVG "program_info/org.sogik.NMCLauncher.svg" PARENT_SCOPE) -set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" 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/nmclauncher.icns" PARENT_SCOPE) +set(Launcher_Branding_MAC_ICON "program_info/NMCLauncher.icon" PARENT_SCOPE) set(Launcher_Branding_ICO "program_info/nmclauncher.ico") set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/nmclauncher.rc" PARENT_SCOPE) @@ -36,11 +40,13 @@ set(Launcher_Branding_LogoQRC "program_info/nmclauncher.qrc" PARENT_SCOPE) set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) -configure_file(org.sogik.NMCLauncher.desktop.in org.sogik.NMCLauncher.desktop) -configure_file(org.sogik.NMCLauncher.metainfo.xml.in org.sogik.NMCLauncher.metainfo.xml) +configure_file(${Launcher_AppID}.desktop.in ${Launcher_AppID}.desktop) +configure_file(${Launcher_AppID}.metainfo.xml.in ${Launcher_AppID}.metainfo.xml) configure_file(nmclauncher.rc.in nmclauncher.rc @ONLY) +configure_file(nmclauncher.qrc.in nmclauncher.qrc @ONLY) configure_file(nmclauncher.manifest.in nmclauncher.manifest @ONLY) configure_file(nmclauncher.ico nmclauncher.ico COPYONLY) +configure_file(${Launcher_SVGFileName} ${Launcher_SVGFileName} COPYONLY) if(MSVC) set(Launcher_MSVC_Redist_NSIS_Section [=[ diff --git a/program_info/NMCLauncher.icon/Assets/block.svg b/program_info/NMCLauncher.icon/Assets/block.svg new file mode 100644 index 000000000..48d033758 --- /dev/null +++ b/program_info/NMCLauncher.icon/Assets/block.svg @@ -0,0 +1,56 @@ + + + NMC Launcher Logo - Block Layer + + + + + + + + + + NMC Launcher Logo - Block Layer + 22/01/2026 + + + NMC Launcher / sogik + + + https://github.com/sogik/NMCLauncher + + + NMC Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/NMCLauncher.icon/Assets/rainbow.svg b/program_info/NMCLauncher.icon/Assets/rainbow.svg new file mode 100644 index 000000000..a63bd4376 --- /dev/null +++ b/program_info/NMCLauncher.icon/Assets/rainbow.svg @@ -0,0 +1,62 @@ + + + NMC Launcher Logo - Color Layer + + + + + + + + + + + + + + + + + NMC Launcher Logo - Color Layer + 22/01/2026 + + + NMC Launcher / sogik + + + https://github.com/sogik/NMCLauncher + + + NMC Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/NMCLauncher.icon/icon.json b/program_info/NMCLauncher.icon/icon.json new file mode 100644 index 000000000..23d8b18b6 --- /dev/null +++ b/program_info/NMCLauncher.icon/icon.json @@ -0,0 +1,72 @@ +{ + "color-space-for-untagged-svg-colors" : "display-p3", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "block.svg", + "name" : "block", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + }, + { + "blur-material-specializations" : [ + { + "value" : 0.5 + }, + { + "appearance" : "dark", + "value" : null + } + ], + "layers" : [ + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : true, + "hidden" : false, + "image-name" : "rainbow.svg", + "name" : "rainbow", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : [ + "macOS" + ] + } +} diff --git a/program_info/PrismLauncher.icon/Assets/block.svg b/program_info/PrismLauncher.icon/Assets/block.svg new file mode 100644 index 000000000..08a80d8bd --- /dev/null +++ b/program_info/PrismLauncher.icon/Assets/block.svg @@ -0,0 +1,91 @@ + + + Prism Launcher Logo + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/PrismLauncher.icon/Assets/rainbow.svg b/program_info/PrismLauncher.icon/Assets/rainbow.svg new file mode 100644 index 000000000..d47bb615a --- /dev/null +++ b/program_info/PrismLauncher.icon/Assets/rainbow.svg @@ -0,0 +1,95 @@ + + + Prism Launcher Logo + + + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/PrismLauncher.icon/icon.json b/program_info/PrismLauncher.icon/icon.json new file mode 100644 index 000000000..23d8b18b6 --- /dev/null +++ b/program_info/PrismLauncher.icon/icon.json @@ -0,0 +1,72 @@ +{ + "color-space-for-untagged-svg-colors" : "display-p3", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "block.svg", + "name" : "block", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + }, + { + "blur-material-specializations" : [ + { + "value" : 0.5 + }, + { + "appearance" : "dark", + "value" : null + } + ], + "layers" : [ + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : true, + "hidden" : false, + "image-name" : "rainbow.svg", + "name" : "rainbow", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : [ + "macOS" + ] + } +} diff --git a/program_info/genicons.sh b/program_info/genicons.sh index 537968bf5..5ab48cfbf 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -1,5 +1,7 @@ #!/bin/bash +LAUNCHER_APPID="org.sogik.NMCLauncher" + svg2png() { input_file="$1" output_file="$2" @@ -9,16 +11,7 @@ svg2png() { inkscape -w "$width" -h "$height" -o "$output_file" "$input_file" } -sipsresize() { - input_file="$1" - output_file="$2" - width="$3" - height="$4" - - sips -z "$width" "$height" "$input_file" --out "$output_file" -} - -if command -v "inkscape" && command -v "icotool"; then +if command -v "inkscape" && command -v "icotool" && command -v "oxipng"; then # Windows ICO d=$(mktemp -d) @@ -30,6 +23,11 @@ if command -v "inkscape" && command -v "icotool"; then svg2png org.sogik.NMCLauncher.svg "$d/nmclauncher_128.png" 128 128 svg2png org.sogik.NMCLauncher.svg "$d/nmclauncher_256.png" 256 256 + oxipng --opt max --strip all --alpha --interlace 0 "$d/nmclauncher_"*".png" + + # Save the 256x256 PNG for Linux packaging + cp -v "$d/nmclauncher_256.png" "${LAUNCHER_APPID}_256.png" + rm nmclauncher.ico && icotool -o nmclauncher.ico -c \ "$d/nmclauncher_256.png" \ "$d/nmclauncher_128.png" \ @@ -40,10 +38,10 @@ if command -v "inkscape" && command -v "icotool"; then "$d/nmclauncher_16.png" else echo "ERROR: Windows icons were NOT generated!" >&2 - echo "ERROR: requires inkscape and icotool in PATH" + echo "ERROR: requires inkscape, icotool and oxipng in PATH" fi -if command -v "inkscape" && command -v "sips" && command -v "iconutil"; then +if command -v "inkscape" && command -v "iconutil" && command -v "oxipng"; then # macOS ICNS d=$(mktemp -d) @@ -51,20 +49,25 @@ if command -v "inkscape" && command -v "sips" && command -v "iconutil"; then mkdir -p "$d" - svg2png org.sogik.NMCLauncher.bigsur.svg "$d/icon_512x512@2x.png" 1024 1024 - sipsresize "$d/icon_512x512@2.png" "$d/icon_16x16.png" 16 16 - sipsresize "$d/icon_512x512@2.png" "$d/icon_16x16@2.png" 32 32 - sipsresize "$d/icon_512x512@2.png" "$d/icon_32x32.png" 32 32 - sipsresize "$d/icon_512x512@2.png" "$d/icon_32x32@2.png" 64 64 - sipsresize "$d/icon_512x512@2.png" "$d/icon_128x128.png" 128 128 - sipsresize "$d/icon_512x512@2.png" "$d/icon_128x128@2.png" 256 256 - sipsresize "$d/icon_512x512@2.png" "$d/icon_256x256.png" 256 256 - sipsresize "$d/icon_512x512@2.png" "$d/icon_256x256@2.png" 512 512 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16.png" 16 16 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16@2x.png" 32 32 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32.png" 32 32 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32@2x.png" 64 64 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128.png" 128 128 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128@2x.png" 256 256 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256.png" 256 256 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256@2x.png" 512 512 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_512x512.png" 512 512 + svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_512x512@2x.png" 1024 1024 + + oxipng --opt max --strip all --alpha --interlace 0 "$d/icon_"*".png" + iconutil -c icns "$d" + cp -v "$d/nmclauncher.icns" . else echo "ERROR: macOS icons were NOT generated!" >&2 - echo "ERROR: requires inkscape, sips and iconutil in PATH" + echo "ERROR: requires inkscape, iconutil and oxipng in PATH" fi # replace icon in themes -cp -v org.sogik.NMCLauncher.svg "../launcher/resources/multimc/scalable/launcher.svg" +cp -v ${LAUNCHER_APPID}.svg "../launcher/resources/multimc/scalable/launcher.svg" diff --git a/program_info/nmclauncher.6.scd b/program_info/nmclauncher.6.scd index 09e745e60..f31555987 100644 --- a/program_info/nmclauncher.6.scd +++ b/program_info/nmclauncher.6.scd @@ -72,10 +72,10 @@ https://github.com/sogik/NMCLauncher/issues GitHub: https://github.com/sogik/NMCLauncher +Main website: https://nmclauncher.nartics.xyz + # AUTHORS sogik Prism Launcher Contributors - -PollyMC Contributors diff --git a/program_info/nmclauncher.icns b/program_info/nmclauncher.icns new file mode 100644 index 000000000..a5e6a8c3a Binary files /dev/null and b/program_info/nmclauncher.icns differ diff --git a/program_info/nmclauncher.manifest.in b/program_info/nmclauncher.manifest.in index 0c705acb6..f5074ff6e 100644 --- a/program_info/nmclauncher.manifest.in +++ b/program_info/nmclauncher.manifest.in @@ -5,7 +5,7 @@ true - + diff --git a/program_info/nmclauncher.qrc b/program_info/nmclauncher.qrc.in similarity index 64% rename from program_info/nmclauncher.qrc rename to program_info/nmclauncher.qrc.in index e6b2487a9..d91bbf96e 100644 --- a/program_info/nmclauncher.qrc +++ b/program_info/nmclauncher.qrc.in @@ -1,6 +1,6 @@ - org.sogik.NMCLauncher.svg + @Launcher_AppID@.svg \ No newline at end of file diff --git a/program_info/nmclauncher.rc.in b/program_info/nmclauncher.rc.in index 0f5d64cb7..c5515033b 100644 --- a/program_info/nmclauncher.rc.in +++ b/program_info/nmclauncher.rc.in @@ -15,10 +15,10 @@ BEGIN BEGIN BLOCK "000004b0" BEGIN - VALUE "CompanyName", "MultiMC & Prism Launcher & PollyMC Contributors" - VALUE "FileDescription", "NMC Launcher" + VALUE "CompanyName", "MultiMC & Prism Launcher & sogik Contributors" + VALUE "FileDescription", "@Launcher_DisplayName@" VALUE "FileVersion", "@Launcher_VERSION_NAME4@" - VALUE "ProductName", "NMC Launcher" + VALUE "ProductName", "@Launcher_DisplayName@" VALUE "ProductVersion", "@Launcher_VERSION_NAME4@" END END @@ -27,3 +27,4 @@ BEGIN VALUE "Translation", 0x0000, 0x04b0 // Unicode END END + diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in new file mode 100644 index 000000000..74c70c5d1 --- /dev/null +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Name=@Launcher_DisplayName@ +Comment=Discover, manage, and play Minecraft instances +Type=Application +Terminal=false +Exec=@Launcher_APP_BINARY_NAME@ %U +StartupNotify=true +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@; diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in new file mode 100644 index 000000000..747e1bc24 --- /dev/null +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -0,0 +1,78 @@ + + + @Launcher_AppID@ + Prism Launcher + Custom Minecraft Launcher to easily manage multiple Minecraft installations at once + + Prism Launcher Contributors + + CC0-1.0 + GPL-3.0-only + +

    Prism Launcher is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.

    +

    Features:

    +
      +
    • Easily install game modifications, such as Fabric, Forge and Quilt
    • +
    • Easily install and update modpacks from the Launcher
    • +
    • Control your Java settings, and enable Mangohud or Gamemode with a toggle
    • +
    • Manage worlds and resource packs from the launcher
    • +
    • See logs and other details easily through a dashboard
    • +
    • Kill Minecraft in case of a crash/freeze
    • +
    • Isolate Minecraft instances to keep everything clean
    • +
    • Install and update mods directly from the launcher
    • +
    • Customize the launcher with themes, and more
    • +
    • And cat :3
    • +
    +
    + + + The main Prism Launcher window + https://prismlauncher.org/img/screenshots/LauncherDark.png + + + Modpack installation + https://prismlauncher.org/img/screenshots/ModpackInstallDark.png + + + Modpack updating + https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png + + + Mod installation + https://prismlauncher.org/img/screenshots/ModInstallDark.png + + + Mod updating + https://prismlauncher.org/img/screenshots/ModUpdateDark.png + + + Instance management + https://prismlauncher.org/img/screenshots/PropertiesDark.png + + + Cat :3 + https://prismlauncher.org/img/screenshots/LauncherCatDark.png + + + Customization + https://prismlauncher.org/img/screenshots/CustomizeDark.png + + + + + + https://prismlauncher.org/ + https://github.com/PrismLauncher/PrismLauncher/issues + https://prismlauncher.org/wiki/overview/faq/ + https://prismlauncher.org/wiki/ + https://opencollective.com/prismlauncher + https://hosted.weblate.org/projects/prismlauncher/launcher + https://prismlauncher.org/discord + https://github.com/PrismLauncher/PrismLauncher + https://github.com/PrismLauncher/PrismLauncher/blob/develop/CONTRIBUTING.md + + moderate + intense + + @Launcher_AppID@.desktop +
    diff --git a/program_info/org.prismlauncher.PrismLauncher_256.png b/program_info/org.prismlauncher.PrismLauncher_256.png new file mode 100644 index 000000000..6f164febe Binary files /dev/null and b/program_info/org.prismlauncher.PrismLauncher_256.png differ diff --git a/program_info/org.sogik.NMCLauncher.desktop.in b/program_info/org.sogik.NMCLauncher.desktop.in index 2bbb814a3..74c70c5d1 100644 --- a/program_info/org.sogik.NMCLauncher.desktop.in +++ b/program_info/org.sogik.NMCLauncher.desktop.in @@ -6,8 +6,8 @@ Type=Application Terminal=false Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true -Icon=org.sogik.@Launcher_CommonName@ -Categories=Game;ActionGame;AdventureGame;Simulation; +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@; diff --git a/program_info/org.sogik.NMCLauncher.metainfo.xml.in b/program_info/org.sogik.NMCLauncher.metainfo.xml.in index 1a2a5902a..36e5da201 100644 --- a/program_info/org.sogik.NMCLauncher.metainfo.xml.in +++ b/program_info/org.sogik.NMCLauncher.metainfo.xml.in @@ -1,16 +1,13 @@ - org.sogik.NMCLauncher - org.sogik.NMCLauncher.desktop + @Launcher_AppID@ NMC Launcher - sogik Custom Minecraft Launcher to easily manage multiple Minecraft installations at once + + sogik + CC0-1.0 - BSD 3-Clause - https://github.com/sogik/NMCLauncher/ - https://prismlauncher.org/wiki/ - https://github.com/sogik/NMCLauncher/issues - https://github.com/sogik/NMCLauncher/blob/dev/CONTRIBUTING.md + GPL-3.0-only

    NMC Launcher is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.

    Features:

    @@ -27,11 +24,53 @@
  • And cat :3
  • + + + The main NMC Launcher window + https://prismlauncher.org/img/screenshots/LauncherDark.png + + + Modpack installation + https://prismlauncher.org/img/screenshots/ModpackInstallDark.png + + + Modpack updating + https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png + + + Mod installation + https://prismlauncher.org/img/screenshots/ModInstallDark.png + + + Mod updating + https://prismlauncher.org/img/screenshots/ModUpdateDark.png + + + Instance management + https://prismlauncher.org/img/screenshots/PropertiesDark.png + + + Cat :3 + https://prismlauncher.org/img/screenshots/LauncherCatDark.png + + + Customization + https://prismlauncher.org/img/screenshots/CustomizeDark.png + + + https://nmclauncher.nartics.xyz/ + https://github.com/sogik/NMCLauncher/issues + https://prismlauncher.org/wiki/overview/faq/ + https://prismlauncher.org/wiki/ + https://hosted.weblate.org/projects/prismlauncher/launcher + https://github.com/sogik/NMCLauncher + https://github.com/sogik/NMCLauncher/blob/dev/CONTRIBUTING.md moderate intense + @Launcher_AppID@.desktop
    diff --git a/program_info/org.sogik.NMCLauncher_256.png b/program_info/org.sogik.NMCLauncher_256.png new file mode 100644 index 000000000..6f164febe Binary files /dev/null and b/program_info/org.sogik.NMCLauncher_256.png differ diff --git a/program_info/prismlauncher.6.scd b/program_info/prismlauncher.6.scd new file mode 100644 index 000000000..e1ebfff32 --- /dev/null +++ b/program_info/prismlauncher.6.scd @@ -0,0 +1,82 @@ +prismlauncher(6) + + +# NAME + +prismlauncher - a launcher and instance manager for Minecraft. + + +# SYNOPSIS + +*prismlauncher* [OPTIONS...] + + +# DESCRIPTION + +Prism Launcher is a custom launcher for Minecraft that allows you to easily manage +multiple installations of Minecraft at once. It also allows you to easily +install and remove mods by simply dragging and dropping. +Here are the current features of Prism Launcher. + +# OPTIONS + +*-d, --dir*=DIRECTORY + Use DIRECTORY as the Prism Launcher root. + +*-l, --launch*=INSTANCE_ID + Launch the instance specified by INSTANCE_ID. + +*--show*=INSTANCE_ID + Show the configuration window of the instance specified by INSTANCE_ID. + +*--alive* + Write a small 'live.check' file after Prism Launcher starts. + +*-h, --help* + Display help text and exit. + +*-v, --version* + Display program version and exit. + +*-a, --profile*=PROFILE + Use the account specified by PROFILE (only valid in combination with --launch). + +# ENVIRONMENT + +The behavior of the launcher can be customized by the following environment +variables, besides other common Qt variables: + +*QT_LOGGING_RULES* + Specifies which logging categories are shown in the logs. One can + enable/disable multiple categories by separating them with a semicolon (;). + + The specific syntax, and alternatives to this setting, can be found at + https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories. + +*QT_MESSAGE_PATTERN* + Specifies the format in which the console output will be shown. + + Available options, as well as syntax, can be viewed at + https://doc.qt.io/qt-6/qtglobal.html#qSetMessagePattern. + +# EXIT STATUS + +*0* + Success + +*1* + Failure (syntax or usage error; configuration error; unexpected error). + +# BUGS + +https://github.com/PrismLauncher/PrismLauncher/issues + +# RESOURCES + +GitHub: https://github.com/PrismLauncher/PrismLauncher + +Main website: https://prismlauncher.org + +# AUTHORS + +Prism Launcher Contributors diff --git a/program_info/prismlauncher.icns b/program_info/prismlauncher.icns index a4c0f7ea4..a5e6a8c3a 100644 Binary files a/program_info/prismlauncher.icns and b/program_info/prismlauncher.icns differ diff --git a/program_info/prismlauncher.ico b/program_info/prismlauncher.ico new file mode 100644 index 000000000..2f0fa67ff Binary files /dev/null and b/program_info/prismlauncher.ico differ diff --git a/program_info/prismlauncher.manifest.in b/program_info/prismlauncher.manifest.in new file mode 100644 index 000000000..f5074ff6e --- /dev/null +++ b/program_info/prismlauncher.manifest.in @@ -0,0 +1,28 @@ + + + + + true + + + + + + + + + + + + + + + + Custom Minecraft launcher for managing multiple installs. + + + + + + + diff --git a/program_info/prismlauncher.qrc.in b/program_info/prismlauncher.qrc.in new file mode 100644 index 000000000..d1e1cdd13 --- /dev/null +++ b/program_info/prismlauncher.qrc.in @@ -0,0 +1,6 @@ + + + + @Launcher_AppID@.svg + + diff --git a/program_info/prismlauncher.rc.in b/program_info/prismlauncher.rc.in new file mode 100644 index 000000000..8f02341e5 --- /dev/null +++ b/program_info/prismlauncher.rc.in @@ -0,0 +1,29 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +IDI_ICON1 ICON DISCARDABLE "prismlauncher.ico" +1 RT_MANIFEST "prismlauncher.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION @Launcher_VERSION_NAME4_COMMA@ +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC & Prism Launcher Contributors" + VALUE "FileDescription", "@Launcher_DisplayName@" + VALUE "FileVersion", "@Launcher_VERSION_NAME4@" + VALUE "ProductName", "@Launcher_DisplayName@" + VALUE "ProductVersion", "@Launcher_VERSION_NAME4@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index dcc127099..e957f4d36 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -134,7 +134,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ WriteRegStr ShCtx "Software\Classes\${APP_ID}" "" `${DESCRIPTION}` WriteRegStr ShCtx "Software\Classes\${APP_ID}\DefaultIcon" "" `${ICON}` ; default open verb - WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell" "" "open" + WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell" "" "open" WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open" "" `${COMMANDTEXT}` WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open\command" "" `${COMMAND}` @@ -153,7 +153,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ !insertmacro APP_SETUP_Def `${DESCRIPTION}` `${ICON}` `${APP_ID}` `${APP_NAME}` `${APP_EXE}` `${COMMANDTEXT}` `${COMMAND}` - # Register "Default Programs" + # Register "Default Programs" WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" "ApplicationDescription" `${DESCRIPTION}` WriteRegStr ShCtx "Software\RegisteredApplications" `${APP_NAME}` "Software\Classes\Applications\${APP_EXE}\Capabilities" @@ -231,7 +231,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ ${IfNot} ${Errors} DeleteRegKey ShCtx "Software\Classes\${APP_ID}\DefaultIcon" ${EndIf} - + # Unregister "Open With" DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}" @@ -256,7 +256,7 @@ VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@ !macro APP_TEARDOWN_DEFAULT APP_ID APP_NAME APP_EXE !insertmacro APP_TEARDOWN_Def `${APP_ID}` `${APP_NAME}` `${APP_EXE}` - + # Unregister "Default Programs" DeleteRegValue ShCtx "Software\RegisteredApplications" `${APP_NAME}` DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" @@ -303,27 +303,27 @@ Function RunUninstall StrCpy $2 $1 1 ; take first char of string StrCmp $2 '"' quoteloop stringloop stringloop: ; get string length - StrCpy $2 $1 1 $3 ; get next char - IntOp $3 $3 + 1 ; index += 1 - StrCmp $2 "" +2 stringloop ; if empty exit loop - IntOp $3 $3 - 1 ; index -= 1 - Goto run + StrCpy $2 $1 1 $3 ; get next char + IntOp $3 $3 + 1 ; index += 1 + StrCmp $2 "" +2 stringloop ; if empty exit loop + IntOp $3 $3 - 1 ; index -= 1 + Goto run quoteloop: ; get string length with quotes removed - StrCmp $3 "" 0 +2 ; if index is set skip quote removal - StrCpy $1 $1 "" 1 ; Remove initial quote - IntOp $3 $3 + 1 ; index += 1 - StrCpy $2 $1 1 $3 ; get next char - StrCmp $2 "" +2 ; if empty exit loop - StrCmp $2 '"' 0 quoteloop ; if ending quote exit loop, else loop + StrCmp $3 "" 0 +2 ; if index is set skip quote removal + StrCpy $1 $1 "" 1 ; Remove initial quote + IntOp $3 $3 + 1 ; index += 1 + StrCpy $2 $1 1 $3 ; get next char + StrCmp $2 "" +2 ; if empty exit loop + StrCmp $2 '"' 0 quoteloop ; if ending quote exit loop, else loop run: - StrCpy $2 $1 $3 ; Path to uninstaller ; (copy string up to ending quote - if it exists) - StrCpy $1 161 ; ERROR_BAD_PATHNAME ; set exit code (it get's overwritten with uninstaller exit code if ExecWait call doesn't error) - GetFullPathName $3 "$2\.." ; $InstDir - IfFileExists "$2" 0 +4 - ExecWait $4 $1 ; The file exists, call the saved command - IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted ; - Delete "$2" ; Delete the uninstaller - RMDir "$3" ; Try to delete $InstDir + StrCpy $2 $1 $3 ; Path to uninstaller ; (copy string up to ending quote - if it exists) + StrCpy $1 161 ; ERROR_BAD_PATHNAME ; set exit code (it get's overwritten with uninstaller exit code if ExecWait call doesn't error) + GetFullPathName $3 "$2\.." ; $InstDir + IfFileExists "$2" 0 +4 + ExecWait $4 $1 ; The file exists, call the saved command + IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted ; + Delete "$2" ; Delete the uninstaller + RMDir "$3" ; Try to delete $InstDir Pop $4 Pop $3 Pop $2 @@ -337,13 +337,13 @@ Section "" UninstallPrevious ${If} $0 == "" ReadRegStr $0 HKCU "${UNINST_KEY}" "UninstallString" ${EndIf} - + ${If} $0 != "" - !insertmacro RunUninstall $0 $0 - ${If} $0 <> 0 - MessageBox MB_YESNO|MB_ICONSTOP "Failed to uninstall, continue anyway?" /SD IDYES IDYES +2 - Abort - ${EndIf} + !insertmacro RunUninstall $0 $0 + ${If} $0 <> 0 + MessageBox MB_YESNO|MB_ICONSTOP "Failed to uninstall, continue anyway?" /SD IDYES IDYES +2 + Abort + ${EndIf} ${EndIf} SectionEnd @@ -393,11 +393,12 @@ Section "@Launcher_DisplayName@" WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" "" WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' -; Write the URL Handler into registry for nmclauncher - WriteRegStr HKCU Software\Classes\nmclauncher "URL Protocol" "" - WriteRegStr HKCU Software\Classes\nmclauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' + ; Write the URL Handler into registry for prismlauncher + 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 uninstall keys for Windows + ; https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} @@ -446,7 +447,7 @@ SectionEnd Section /o "Shell Association (Open-With dialog)" SHELL_ASSOC !insertmacro APP_SETUP `${APPDESCRIPTION}` `${APPICON}` `${APPID}` `${APPCMDTEXT}` `${APPEXE}` `${APPCMDTEXT}` '$INSTDIR\${APPEXE} -I "%1"' - + !insertmacro APP_ASSOCIATE_DEFAULT ".mrpack" `${APPID}` `${APPEXE}` true !insertmacro APP_ASSOCIATE ".zip" `${APPID}` `${APPEXE}` false @@ -488,7 +489,7 @@ Section "Uninstall" SectionEnd Section -un.ShellAssoc - + !insertmacro APP_TEARDOWN_DEFAULT `${APPID}` `${APPNAME}` `${APPEXE}` !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` `${APPEXE}` diff --git a/scripts/compress_images.sh b/scripts/compress_images.sh new file mode 100644 index 000000000..136059ff8 --- /dev/null +++ b/scripts/compress_images.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +## If current working dirctory is ./scripts, ask to invoke from one directory up +if [ ! -d "scripts" ]; then + echo "Please run this script from the root directory of the project" + exit 1 +fi + +find . -type f -name '*.png' -not -path '*/libraries/*' -exec oxipng --opt max --strip all --alpha --interlace 0 {} \; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2dedb47cc..2165cd03d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,9 +21,6 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourcePackParse) -ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME ResourceModel) - ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) @@ -62,3 +59,7 @@ ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VE ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) + + +ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME XmlLogs) diff --git a/tests/DataPackParse_test.cpp b/tests/DataPackParse_test.cpp index cd6ae8e8f..14f80858f 100644 --- a/tests/DataPackParse_test.cpp +++ b/tests/DataPackParse_test.cpp @@ -38,7 +38,7 @@ class DataPackParseTest : public QObject { QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip"); DataPack pack{ QFileInfo(zip_dp) }; - bool valid = DataPackUtils::processZIP(pack); + bool valid = DataPackUtils::processZIP(&pack); QVERIFY(pack.packFormat() == 4); QVERIFY(pack.description() == "Some data pack 2 boobgaloo"); @@ -52,7 +52,7 @@ class DataPackParseTest : public QObject { QString folder_dp = FS::PathCombine(source, "test_folder"); DataPack pack{ QFileInfo(folder_dp) }; - bool valid = DataPackUtils::processFolder(pack); + bool valid = DataPackUtils::processFolder(&pack); QVERIFY(pack.packFormat() == 10); QVERIFY(pack.description() == "Some data pack, maybe"); @@ -66,7 +66,7 @@ class DataPackParseTest : public QObject { QString folder_dp = FS::PathCombine(source, "another_test_folder"); DataPack pack{ QFileInfo(folder_dp) }; - bool valid = DataPackUtils::process(pack); + bool valid = DataPackUtils::process(&pack); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "Some data pack three, leaves on the tree"); diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h deleted file mode 100644 index 35de95151..000000000 --- a/tests/DummyResourceAPI.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include - -#include - -class SearchTask : public Task { - Q_OBJECT - - public: - void executeTask() override { emitSucceeded(); } -}; - -class DummyResourceAPI : public ResourceAPI { - public: - static auto searchRequestResult() - { - static QByteArray json_response = - "{\"hits\":[" - "{" - "\"author\":\"flowln\"," - "\"description\":\"the bestest mod\"," - "\"project_id\":\"something\"," - "\"project_type\":\"mod\"," - "\"slug\":\"bip_bop\"," - "\"title\":\"AAAAAAAA\"," - "\"versions\":[\"2.71\"]" - "}" - "]}"; - - return QJsonDocument::fromJson(json_response); - } - - DummyResourceAPI() : ResourceAPI() {} - [[nodiscard]] auto getSortingMethods() const -> QList override { return {}; } - - [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override - { - auto task = makeShared(); - QObject::connect(task.get(), &Task::succeeded, [=] { - auto json = searchRequestResult(); - callbacks.on_succeed(json); - }); - return task; - } -}; diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 1d3cee85f..37e5e1201 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -2,32 +2,15 @@ #include #include #include +#include #include #include #include -// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header - -#ifdef __APPLE__ -#include // for deployment target to support pre-catalina targets without std::fs -#endif // __APPLE__ - -#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) -#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) -#define GHC_USE_STD_FS #include namespace fs = std::filesystem; -#endif // MacOS min version check -#endif // Other OSes version check - -#ifndef GHC_USE_STD_FS -#include -namespace fs = ghc::filesystem; -#endif - -#include class LinkTask : public Task { Q_OBJECT @@ -42,7 +25,7 @@ class LinkTask : public Task { ~LinkTask() { delete m_lnk; } - void matcher(const IPathMatcher* filter) { m_lnk->matcher(filter); } + void matcher(Filter filter) { m_lnk->matcher(filter); } void linkRecursively(bool recursive) { @@ -63,7 +46,7 @@ class LinkTask : public Task { qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "atempting to run with privelage"; - connect(m_lnk, &FS::create_link::finishedPrivileged, this, [&](bool gotResults) { + connect(m_lnk, &FS::create_link::finishedPrivileged, this, [this](bool gotResults) { if (gotResults) { emitSucceeded(); } else { @@ -84,7 +67,9 @@ class LinkTask : public Task { } FS::create_link* m_lnk; - [[maybe_unused]] bool m_useHard = false; +#if defined Q_OS_WIN32 + bool m_useHard = false; +#endif bool m_linkRecursive = true; }; @@ -113,22 +98,12 @@ class FileSystemTest : public QObject { QTest::addColumn("path1"); QTest::addColumn("path2"); - QTest::newRow("qt 1") << "/abc/def/ghi/jkl" - << "/abc/def" - << "ghi/jkl"; - QTest::newRow("qt 2") << "/abc/def/ghi/jkl" - << "/abc/def/" - << "ghi/jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; #if defined(Q_OS_WIN) - QTest::newRow("win native, from C:") << "C:/abc" - << "C:" - << "abc"; - QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\def" - << "ghi\\jkl"; - QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\def\\" - << "ghi\\jkl"; + QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; + QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; + QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; #endif } @@ -148,39 +123,15 @@ class FileSystemTest : public QObject { QTest::addColumn("path2"); QTest::addColumn("path3"); - QTest::newRow("qt 1") << "/abc/def/ghi/jkl" - << "/abc" - << "def" - << "ghi/jkl"; - QTest::newRow("qt 2") << "/abc/def/ghi/jkl" - << "/abc/" - << "def" - << "ghi/jkl"; - QTest::newRow("qt 3") << "/abc/def/ghi/jkl" - << "/abc" - << "def/" - << "ghi/jkl"; - QTest::newRow("qt 4") << "/abc/def/ghi/jkl" - << "/abc/" - << "def/" - << "ghi/jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; + QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; + QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; #if defined(Q_OS_WIN) - QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" - << "C:\\abc" - << "def" - << "ghi\\jkl"; - QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\" - << "def" - << "ghi\\jkl"; - QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" - << "C:\\abc" - << "def\\" - << "ghi\\jkl"; - QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" - << "C:\\abc\\" - << "def" - << "ghi\\jkl"; + QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; + QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; + QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; + QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; #endif } @@ -237,8 +188,8 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - c.matcher(&re); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); + c.matcher(re); c(); for (auto entry : target_dir.entryList()) { @@ -270,8 +221,8 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - c.matcher(&re); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); + c.matcher(re); c.whitelist(true); c(); @@ -369,11 +320,11 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(false); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -462,14 +413,14 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - lnk_tsk.matcher(&re); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); + lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -508,15 +459,15 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher re("[.]?mcmeta"); - lnk_tsk.matcher(&re); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); + lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; @@ -556,11 +507,11 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; @@ -604,11 +555,11 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(file, target_dir.filePath("pack.mcmeta")); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files; @@ -639,11 +590,11 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(0); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); QVERIFY(!QFileInfo(target_dir.path()).isSymLink()); @@ -689,13 +640,13 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(-1); - QObject::connect(&lnk_tsk, &Task::finished, - [&] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); - QVERIFY2(QTest::qWaitFor([&]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); - std::function verify_check = [&](QString check_path) { + std::function verify_check = [&verify_check](QString check_path) { QDir check_dir(check_path); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : check_dir.entryList(filter)) { diff --git a/tests/INIFile_test.cpp b/tests/INIFile_test.cpp index 95730e244..559600212 100644 --- a/tests/INIFile_test.cpp +++ b/tests/INIFile_test.cpp @@ -110,7 +110,7 @@ Wrapperommand=)"; f2.loadFile(fileName); QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); - QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif @@ -151,7 +151,7 @@ Wrapperommand=)"; f2.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); - QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif @@ -185,7 +185,7 @@ PreLaunchCommand=)"; INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); - QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif diff --git a/tests/Library_test.cpp b/tests/Library_test.cpp index 9826abbdf..59bc90c0c 100644 --- a/tests/Library_test.cpp +++ b/tests/Library_test.cpp @@ -48,7 +48,10 @@ class LibraryTest : public QObject { LibraryPtr readMojangJson(const QString path) { QFile jsonFile(path); - jsonFile.open(QIODevice::ReadOnly); + if (!jsonFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file '" << jsonFile.fileName() << "' for reading!"; + return LibraryPtr(); + } auto data = jsonFile.readAll(); jsonFile.close(); ProblemContainer problems; diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp index 9979a9fa6..c5c41388b 100644 --- a/tests/MetaComponentParse_test.cpp +++ b/tests/MetaComponentParse_test.cpp @@ -41,7 +41,7 @@ #include -#include +#include class MetaComponentParseTest : public QObject { Q_OBJECT @@ -69,7 +69,7 @@ class MetaComponentParseTest : public QObject { QString expected = expected_json.toString(); - QString processed = ResourcePackUtils::processComponent(description_json); + QString processed = DataPackUtils::processComponent(description_json); QCOMPARE(processed, expected); } diff --git a/tests/MojangVersionFormat_test.cpp b/tests/MojangVersionFormat_test.cpp index 9ff5d78c0..3c8d3ffc5 100644 --- a/tests/MojangVersionFormat_test.cpp +++ b/tests/MojangVersionFormat_test.cpp @@ -9,7 +9,10 @@ class MojangVersionFormatTest : public QObject { static QJsonDocument readJson(const QString path) { QFile jsonFile(path); - jsonFile.open(QIODevice::ReadOnly); + if (!jsonFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file '" << jsonFile.fileName() << "' for reading!"; + return QJsonDocument(); + } auto data = jsonFile.readAll(); jsonFile.close(); return QJsonDocument::fromJson(data); @@ -17,7 +20,10 @@ class MojangVersionFormatTest : public QObject { static void writeJson(const char* file, QJsonDocument doc) { QFile jsonFile(file); - jsonFile.open(QIODevice::WriteOnly | QIODevice::Text); + if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + qCritical() << "Failed to open file '" << jsonFile.fileName() << "' for writing!"; + return; + } auto data = doc.toJson(QJsonDocument::Indented); qDebug() << QString::fromUtf8(data); jsonFile.write(data); diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp index e4abda9f9..1fcb1b9f9 100644 --- a/tests/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -19,6 +19,7 @@ #include #include +#include "modplatform/ModIndex.h" #include @@ -42,7 +43,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); - QCOMPARE(metadata.side, Packwiz::V1::Side::ClientSide); + QCOMPARE(metadata.side, ModPlatform::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); @@ -72,7 +73,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); - QCOMPARE(metadata.side, Packwiz::V1::Side::UniversalSide); + QCOMPARE(metadata.side, ModPlatform::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); diff --git a/tests/ResourceFolderModel_test.cpp b/tests/ResourceFolderModel_test.cpp index 350ab615e..f2201a5e9 100644 --- a/tests/ResourceFolderModel_test.cpp +++ b/tests/ResourceFolderModel_test.cpp @@ -87,7 +87,7 @@ class ResourceFolderModelTest : public QObject { QEventLoop loop; - ModFolderModel m(tempDir.path(), nullptr, true); + ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); @@ -96,7 +96,7 @@ class ResourceFolderModelTest : public QObject { expire_timer.setSingleShot(true); expire_timer.start(4000); - m.installMod(folder); + m.installResource(folder); loop.exec(); @@ -111,7 +111,7 @@ class ResourceFolderModelTest : public QObject { QString folder = source + '/'; QTemporaryDir tempDir; QEventLoop loop; - ModFolderModel m(tempDir.path(), nullptr, true); + ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); @@ -120,7 +120,7 @@ class ResourceFolderModelTest : public QObject { expire_timer.setSingleShot(true); expire_timer.start(4000); - m.installMod(folder); + m.installResource(folder); loop.exec(); @@ -134,7 +134,7 @@ class ResourceFolderModelTest : public QObject { void test_addFromWatch() { QString source = QFINDTESTDATA("testdata/ResourceFolderModel"); - ModFolderModel model(source, nullptr); + ModFolderModel model(source, nullptr, false, true); QCOMPARE(model.size(), 0); @@ -154,7 +154,7 @@ class ResourceFolderModelTest : public QObject { QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; - ResourceFolderModel model(QDir(tmp.path()), nullptr); + ResourceFolderModel model(QDir(tmp.path()), nullptr, false, false); QCOMPARE(model.size(), 0); @@ -199,7 +199,7 @@ class ResourceFolderModelTest : public QObject { QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; - ResourceFolderModel model(tmp.path(), nullptr); + ResourceFolderModel model(tmp.path(), nullptr, false, false); QCOMPARE(model.size(), 0); @@ -210,7 +210,7 @@ class ResourceFolderModelTest : public QObject { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } - for (auto res : model.all()) + for (auto res : model.allResources()) qDebug() << res->name(); QCOMPARE(model.size(), 2); diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp deleted file mode 100644 index b589758aa..000000000 --- a/tests/ResourceModel_test.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include - -#include - -#include - -#include "DummyResourceAPI.h" - -using ResourceDownload::ResourceModel; - -#define EXEC_TASK(EXEC) \ - QEventLoop loop; \ - \ - connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \ - \ - QTimer expire_timer; \ - expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ - expire_timer.setSingleShot(true); \ - expire_timer.start(4000); \ - \ - EXEC; \ - if (model->hasActiveSearchJob()) \ - loop.exec(); \ - \ - QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \ - expire_timer.stop(); \ - \ - disconnect(model, nullptr, &loop, nullptr) - -class ResourceModelTest; - -class DummyResourceModel : public ResourceModel { - Q_OBJECT - - friend class ResourceModelTest; - - public: - DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} - ~DummyResourceModel() {} - - [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; } - - ResourceAPI::SearchArgs createSearchArguments() override { return {}; } - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; } - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; } - - QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } - - void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override - { - pack.authors.append({ Json::requireString(obj, "author"), "" }); - pack.description = Json::requireString(obj, "description"); - pack.addonId = Json::requireString(obj, "project_id"); - } -}; - -class ResourceModelTest : public QObject { - Q_OBJECT - private slots: - void test_abstract_item_model() - { - auto dummy = DummyResourceModel(); - auto tester = QAbstractItemModelTester(&dummy); - } - - void test_search() - { - auto model = new DummyResourceModel; - - QVERIFY(model->m_packs.isEmpty()); - - EXEC_TASK(model->search()); - - QVERIFY(model->m_packs.size() == 1); - QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished); - - auto processed_pack = model->m_packs.at(0); - auto search_json = DummyResourceAPI::searchRequestResult(); - auto processed_response = model->documentToArray(search_json).first().toObject(); - - QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id")); - QVERIFY(processed_pack->description == Json::requireString(processed_response, "description")); - QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author")); - - delete model; - } -}; - -QTEST_GUILESS_MAIN(ResourceModelTest) - -#include "ResourceModel_test.moc" - -#include "moc_DummyResourceAPI.cpp" diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index e1092167d..5400d888a 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -18,11 +18,11 @@ #include #include +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include #include -#include class ResourcePackParseTest : public QObject { Q_OBJECT @@ -35,7 +35,7 @@ class ResourcePackParseTest : public QObject { QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack{ QFileInfo(zip_rp) }; - bool valid = ResourcePackUtils::processZIP(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::processZIP(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 3); QVERIFY(pack.description() == @@ -51,7 +51,7 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; - bool valid = ResourcePackUtils::processFolder(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::processFolder(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 1); QVERIFY(pack.description() == "Some resource pack maybe"); @@ -65,11 +65,11 @@ class ResourcePackParseTest : public QObject { QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; - bool valid = ResourcePackUtils::process(pack, ResourcePackUtils::ProcessingLevel::BasicInfoOnly); + bool valid = DataPackUtils::process(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); - QVERIFY(valid == false); // no assets dir + QVERIFY(valid == true); // no assets dir but it is still valid based on https://minecraft.wiki/w/Resource_pack } }; diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 463e78b42..1c95f702d 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -66,7 +66,7 @@ class BigConcurrentTaskThread : public QThread { } connect(&big_task, &Task::finished, this, &QThread::quit); - connect(&m_deadline, &QTimer::timeout, this, [&] { + connect(&m_deadline, &QTimer::timeout, this, [this] { passed_the_deadline = true; quit(); }); @@ -127,11 +127,11 @@ class TaskTest : public QObject { void test_basicRun() { BasicTask t; - QObject::connect(&t, &Task::finished, - [&] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&t, &Task::finished, + [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicConcurrentRun() @@ -146,7 +146,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -154,7 +154,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } // Tests if starting new tasks after the 6 initial ones is working @@ -182,7 +182,7 @@ class TaskTest : public QObject { t.addTask(t8); t.addTask(t9); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -196,7 +196,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicSequentialRun() @@ -211,7 +211,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -219,7 +219,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicMultipleOptionsRun() @@ -234,7 +234,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(!t2->wasSuccessful()); @@ -242,7 +242,7 @@ class TaskTest : public QObject { }); t.start(); - QVERIFY2(QTest::qWaitFor([&]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); + QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_stackOverflowInConcurrentTask() diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index 4c67cc544..15be63f62 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -106,7 +106,10 @@ class VersionTest : public QObject { QFile vector_file{ test_vector_dir.absoluteFilePath("test_vectors.txt") }; - vector_file.open(QFile::OpenModeFlag::ReadOnly); + if (!vector_file.open(QFile::OpenModeFlag::ReadOnly)) { + qCritical() << "Failed to open file '" << vector_file.fileName() << "' for reading!"; + return; + } int test_number = 0; const QString test_name_template{ "FlexVer test #%1 (%2)" }; diff --git a/tests/XmlLogs_test.cpp b/tests/XmlLogs_test.cpp new file mode 100644 index 000000000..31ffbba1c --- /dev/null +++ b/tests/XmlLogs_test.cpp @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +class XmlLogParseTest : public QObject { + Q_OBJECT + + private slots: + + void parseXml_data() + { + QString source = QFINDTESTDATA("testdata/TestLogs"); + + QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); + QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); + QStringList shortTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList shortTextLevels; + shortTextLevels.reserve(24); + std::transform(shortTextLevels_s.cbegin(), shortTextLevels_s.cend(), std::back_inserter(shortTextLevels), + [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); + + QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); + QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); + QStringList longTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + QStringList longTextLevelsXml_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-xml-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList longTextLevelsPlain; + longTextLevelsPlain.reserve(974); + std::transform(longTextLevels_s.cbegin(), longTextLevels_s.cend(), std::back_inserter(longTextLevelsPlain), + [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); + QList longTextLevelsXml; + longTextLevelsXml.reserve(896); + std::transform(longTextLevelsXml_s.cbegin(), longTextLevelsXml_s.cend(), std::back_inserter(longTextLevelsXml), + [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); + + QTest::addColumn("log"); + QTest::addColumn("num_entries"); + QTest::addColumn>("entry_levels"); + + QTest::newRow("short-vanilla-plain") << shortText << 25 << shortTextLevels; + QTest::newRow("short-vanilla-xml") << shortXml << 25 << shortTextLevels; + QTest::newRow("long-forge-plain") << longText << 945 << longTextLevelsPlain; + QTest::newRow("long-forge-xml") << longXml << 869 << longTextLevelsXml; + } + + void parseXml() + { + QFETCH(QString, log); + QFETCH(int, num_entries); + QFETCH(QList, entry_levels); + + QList> entries = {}; + + QBENCHMARK + { + entries = parseLines(log.split(QRegularExpression("\n|\r\n|\r"))); + } + + QCOMPARE(entries.length(), num_entries); + + QList levels = {}; + + std::transform(entries.cbegin(), entries.cend(), std::back_inserter(levels), + [](std::pair entry) { return entry.first; }); + + QCOMPARE(levels, entry_levels); + } + + private: + LogParser m_parser; + + QList> parseLines(const QStringList& lines) + { + QList> out; + MessageLevel last = MessageLevel::Unknown; + + for (const auto& line : lines) { + m_parser.appendLine(line); + + auto items = m_parser.parseAvailable(); + for (const auto& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + out.append(std::make_pair(entry.level, msg)); + last = entry.level; + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + auto level = LogParser::guessLevel(msg, last); + + out.append(std::make_pair(level, msg)); + last = level; + } + } + } + return out; + } +}; + +QTEST_GUILESS_MAIN(XmlLogParseTest) + +#include "XmlLogs_test.moc" diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log new file mode 100644 index 000000000..c0775ebb7 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log @@ -0,0 +1,947 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG +2025-04-18 12:47:23,932 main WARN Advanced terminal features are not available in this environment +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher running: args [--username, Ryexandrite, --version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --assetIndex, 5, --uuid, , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480] +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher 10.0.9+10.0.9+main.dcd20f30 starting: java version 17.0.8 by Microsoft; OS Linux arch amd64 version 6.6.85 +[12:47:24] [main/INFO] [ne.mi.fm.lo.ImmediateWindowHandler/]: Loading ImmediateWindowProvider fmlearlywindow +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Trying GL version 4.6 +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Requested GL version 4.6 got version 4.6 +[12:47:24] [main/INFO] [mixin/]: SpongePowered MIXIN Subsystem Version=0.8.5 Source=union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar%23140!/ Service=ModLauncher Env=CLIENT +[12:47:24] [pool-2-thread-1/INFO] [EARLYDISPLAY/]: GL info: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) GL version 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f), AMD +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/fmlcore/1.20.1-47.2.6/fmlcore-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/javafmllanguage/1.20.1-47.2.6/javafmllanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/lowcodelanguage/1.20.1-47.2.6/lowcodelanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/mclanguage/1.20.1-47.2.6/mclanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.ja.se.JarSelector/]: Attempted to select two dependency jars from JarJar which have the same identification: Mod File: and Mod File: . Using Mod File: +[12:47:25] [main/INFO] [ne.mi.fm.lo.mo.JarInJarDependencyLocator/]: Found 28 dependencies adding them to mods collection +[12:47:28] [main/ERROR] [mixin/]: Mixin config dynamiclightsreforged.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [mixin/]: Compatibility level set to JAVA_17 +[12:47:28] [main/ERROR] [mixin/]: Mixin config mixins.satin.client.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config firstperson.mixins.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config yacl.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [cp.mo.mo.LaunchServiceHandler/MODLAUNCHER]: Launching target 'forgeclient' with arguments [--version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --uuid, , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480] +[12:47:28] [main/INFO] [co.ab.sa.co.Saturn/]: Loaded Saturn config file with 4 configurable options +[12:47:28] [main/INFO] [ModernFix/]: Loaded configuration file for ModernFix 5.18.1+mc1.20.1: 83 options available, 1 override(s) found +[12:47:28] [main/WARN] [ModernFix/]: Option 'mixin.perf.thread_priorities' overriden (by mods [smoothboot]) to 'false' +[12:47:28] [main/INFO] [ModernFix/]: Applying Nashorn fix +[12:47:28] [main/INFO] [ModernFix/]: Applied Forge config corruption patch +[12:47:28] [main/INFO] [fpsreducer/]: OptiFine was NOT detected. +[12:47:28] [main/INFO] [fpsreducer/]: OptiFabric was NOT detected. +[12:47:28] [main/WARN] [EmbeddiumConfig/]: Mod 'tfc' attempted to override option 'mixin.features.fast_biome_colors', which doesn't exist, ignoring +[12:47:28] [main/INFO] [Embeddium/]: Loaded configuration file for Embeddium: 205 options available, 3 override(s) found +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Searching for graphics cards... +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Found graphics card: GraphicsAdapterInfo[vendor=AMD, name=Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT], version=unknown] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: Sodium has applied one or more workarounds to prevent crashes or other issues on your system: [NO_ERROR_CONTEXT_UNSUPPORTED] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: This is not necessarily an issue, but it may result in certain features or optimizations being disabled. You can sometimes fix these issues by upgrading your graphics driver. +[12:47:28] [main/INFO] [Radium Config/]: Loaded configuration file for Radium: 125 options available, 7 override(s) found +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-common-refmap.json' for carpeted-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-forge-refmap.json' for carpeted.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'emi-forge-refmap.json' for emi-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-common-refmap.json' for ftbfiltersystem-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-forge-refmap.json' for ftbfiltersystem.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/INFO] [Puzzles Lib/]: Loading 160 mods: + - additionalplacements 1.8.0 + - ae2 15.2.13 + - ae2insertexportcard 1.20.1-1.3.0 + - ae2netanalyser 1.20-1.0.6-forge + - ae2wtlib 15.2.3-forge + - aiimprovements 0.5.2 + - ambientsounds 6.1.1 + - architectury 9.2.14 + - astikorcarts 1.1.8 + - attributefix 21.0.4 + - balm 7.3.9 + \-- kuma_api 20.1.8 + - barrels_2012 2.1 + - betterf3 7.0.2 + - betterfoliage 5.0.2 + - betterpingdisplay 1.1 + - betterthirdperson 1.9.0 + - blur 3.1.1 + \-- satin 1.20.1+1.15.0-SNAPSHOT + - carpeted 1.20-1.4 + - carryon 2.1.2.7 + \-- mixinextras 0.2.0-beta.6 + - catalogue 1.8.0 + - chat_heads 0.13.9 + - cherishedworlds 6.1.7+1.20.1 + - clienttweaks 11.1.0 + - cloth_config 11.1.136 + - clumps 12.0.0.4 + - computercraft 1.113.1 + - controlling 12.0.2 + - coralstfc 1.0.0 + - corpse 1.20.1-1.0.19 + - cosmeticarmorreworked 1.20.1-v1a + - craftingtweaks 18.2.5 + - craftpresence 2.5.0 + - create 0.5.1.f + \-- flywheel 0.6.10-7 + - create_connected 0.8.2-mc1.20.1 + - createaddition 1.20.1-1.2.4c + - creativecore 2.12.15 + - cucumber 7.0.12 + - cupboard 1.20.1-2.7 + - curios 5.10.0+1.20.1 + - defaultoptions 18.0.1 + - do_a_barrel_roll 3.5.6+1.20.1 + - drippyloadingscreen 3.0.1 + - dynamiclightsreforged 1.20.1_v1.6.0 + - embeddium 0.3.19+mc1.20.1 + \-- rubidium 0.7.1 + - embeddiumplus 1.2.12 + - emi 1.1.7+1.20.1+forge + - enhancedvisuals 1.8.1 + - etched 3.0.2 + - everycomp 1.20-2.7.12 + - expatternprovider 1.20-1.1.14-forge + - exposure 1.7.7 + - fallingtrees 0.12.7 + - fancymenu 3.2.3 + - ferritecore 6.0.1 + - firmaciv 0.2.10-alpha-1.20.1 + - firmalife 2.1.15 + - firstperson 2.4.5 + - flickerfix 4.0.1 + - forge 47.2.6 + - fpsreducer 1.20-2.5 + - framedblocks 9.3.1 + - ftbbackups2 1.0.23 + - ftbessentials 2001.2.2 + - ftbfiltersystem 1.0.2 + - ftblibrary 2001.2.4 + - ftbquests 2001.4.8 + - ftbranks 2001.1.3 + - ftbteams 2001.3.0 + - ftbxmodcompat 2.1.1 + - gcyr 0.1.8 + - getittogetherdrops 1.3 + - glodium 1.20-1.5-forge + - gtceu 1.2.3.a + |-- configuration 2.2.0 + \-- ldlib 1.0.25.j + - hangglider 8.0.1 + - immediatelyfast 1.2.18+1.20.4 + - inventoryhud 3.4.26 + - invtweaks 1.1.0 + - itemphysiclite 1.6.5 + - jade 11.9.4+forge + - jadeaddons 5.2.2 + - jei 15.3.0.8 + - konkrete 1.8.0 + - ksyxis 1.3.2 + - kubejs 2001.6.5-build.14 + - kubejs_create 2001.2.5-build.2 + - kubejs_tfc 1.20.1-1.1.3 + - letmedespawn 1.3.2b + - lootjs 1.20.1-2.12.0 + - megacells 2.4.4-1.20.1 + - melody 1.0.2 + - memoryleakfix 1.1.5 + - merequester 1.20.1-1.1.5 + - minecraft 1.20.1 + - modelfix 1.15 + - modernfix 5.18.1+mc1.20.1 + - moonlight 1.20-2.13.51 + - morered 4.0.0.4 + |-- jumbofurnace 4.0.0.5 + \-- useitemonblockevent 1.0.0.2 + - mousetweaks 2.25.1 + - myserveriscompatible 1.0 + - nanhealthfixer 1.20.1-0.0.1 + - nerb 0.4.1 + - noisium 2.3.0+mc1.20-1.20.1 + - noreportbutton 1.5.0 + - notenoughanimations 1.7.6 + - octolib 0.4.2 + - oculus 1.7.0 + - openpartiesandclaims 0.23.2 + - packetfixer 1.4.2 + - pandalib 0.4.2 + - patchouli 1.20.1-84-FORGE + - pickupnotifier 8.0.0 + - placebo 8.6.2 + - playerrevive 2.0.27 + - polylib 2000.0.3-build.143 + - puzzleslib 8.1.23 + \-- puzzlesaccessapi 8.0.7 + - radium 0.12.3+git.50c5c33 + - railways 1.6.4+forge-mc1.20.1 + - recipeessentials 1.20.1-3.6 + - rhino 2001.2.2-build.18 + - saturn 0.1.3 + - searchables 1.0.3 + - shimmer 1.20.1-0.2.4 + - showcaseitem 1.20.1-1.2 + - simplylight 1.20.1-1.4.6-build.50 + - smoothboot 0.0.4 + - sophisticatedbackpacks 3.20.5.1044 + - sophisticatedcore 0.6.22.611 + - supermartijn642configlib 1.1.8 + - supermartijn642corelib 1.1.17 + - tfc 3.2.12 + - tfc_tumbleweed 1.2.2 + - tfcagedalcohol 2.1 + - tfcambiental 1.20.1-3.3.0 + - tfcastikorcarts 1.1.8.2 + - tfcchannelcasting 0.2.3-beta + - tfcea 0.0.2 + - tfcgroomer 1.20.1-0.1.2 + - tfchotornot 1.0.4 + - tfcvesseltooltip 1.1 + - tfg 0.5.9 + - toofast 0.4.3.5 + - toolbelt 1.20.01 + - treetap 1.20.1-0.4.0 + - tumbleweed 0.5.5 + - unilib 1.0.2 + - uteamcore 5.1.4.312 + - waterflasks 3.0.3 + - xaerominimap 24.4.0 + - xaeroworldmap 1.39.0 + - yeetusexperimentus 2.3.1-build.6+mc1.20.1 + - yet_another_config_lib_v3 3.5.0+1.20.1-forge +[12:47:28] [main/WARN] [mixin/]: Reference map 'packetfixer-forge-forge-refmap.json' for packetfixer-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'tfchotornot.refmap.json' for tfchotornot.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/WARN] [mixin/]: Error loading class: mezz/modnametooltip/TooltipEventHandler (java.lang.ClassNotFoundException: mezz.modnametooltip.TooltipEventHandler) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/ClientHelperImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.ClientHelperImpl) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl) +[12:47:29] [main/INFO] [co.cu.Cupboard/]: Loaded config for: recipeessentials.json +[12:47:30] [main/WARN] [mixin/]: Error loading class: loaderCommon/forge/com/seibel/distanthorizons/common/wrappers/worldGeneration/mimicObject/ChunkLoader (java.lang.ClassNotFoundException: loaderCommon.forge.com.seibel.distanthorizons.common.wrappers.worldGeneration.mimicObject.ChunkLoader) +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.RenderSystemMixin will be applied. +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.WindowMixin will NOT be applied because OptiFine was NOT detected. +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin2 false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ClientPacketListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.CommandSuggestionSuggestionsListMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ConnectionMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.DownloadedPackSourceMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.FontStringRenderOutputMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageLineMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.HttpTextureMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.PlayerChatMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.SkinManagerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.compat.EmojifulMixin true false +[12:47:30] [main/WARN] [mixin/]: Error loading class: dan200/computercraft/shared/integration/jei/JEIComputerCraft (java.lang.ClassNotFoundException: dan200.computercraft.shared.integration.jei.JEIComputerCraft) +[12:47:30] [main/WARN] [mixin/]: @Mixin target dan200.computercraft.shared.integration.jei.JEIComputerCraft was not found tfg.mixins.json:common.cc.JEIComputerCraftMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/slab/CopycatSlabBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/board/CopycatBoardBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: me/jellysquid/mods/lithium/common/ai/pathing/PathNodeDefaults (java.lang.ClassNotFoundException: me.jellysquid.mods.lithium.common.ai.pathing.PathNodeDefaults) +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'alloc.blockstate.StateMixin' as option 'mixin.alloc.blockstate' (added by mods [ferritecore]) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.fluid.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.intersection.WorldMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.movement.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.AbstractMinecartEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.BoatEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityPredicatesMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityTrackingSectionMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.LivingEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'world.player_chunk_tick.ThreadedAnvilChunkStorageMixin' as option 'mixin.world.player_chunk_tick' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [mixin/]: Error loading class: org/cyclops/integrateddynamics/block/BlockCable (java.lang.ClassNotFoundException: org.cyclops.integrateddynamics.block.BlockCable) +[12:47:30] [main/WARN] [mixin/]: @Mixin target org.cyclops.integrateddynamics.block.BlockCable was not found mixins.epp.json:MixinBlockCable +[12:47:30] [main/WARN] [mixin/]: Error loading class: blusunrize/immersiveengineering/api/wires/GlobalWireNetwork (java.lang.ClassNotFoundException: blusunrize.immersiveengineering.api.wires.GlobalWireNetwork) +[12:47:30] [main/WARN] [mixin/]: @Mixin target blusunrize.immersiveengineering.api.wires.GlobalWireNetwork was not found mixins.epp.json:MixinGlobalWireNetwork +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:TornadoHelperMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:client.TornadoHelperMixin +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Will be applying 3 memory leak fixes! +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Currently enabled memory leak fixes: [targetEntityLeak, biomeTemperatureLeak, hugeScreenshotLeak] +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.WorldRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.ClientWorldMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.BackgroundRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.GlyphRendererMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.FontSetMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.shadows.EntityRenderDispatcherMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.ModelPartMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.CuboidMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.cull.EntityRendererMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [mixin/]: Error loading class: org/jetbrains/annotations/ApiStatus$Internal (java.lang.ClassNotFoundException: org.jetbrains.annotations.ApiStatus$Internal) +[12:47:31] [main/INFO] [MixinExtras|Service/]: Initializing MixinExtras via com.llamalad7.mixinextras.service.MixinExtrasServiceImpl(version=0.4.1). +[12:47:31] [main/INFO] [Smooth Boot (Reloaded)/]: Smooth Boot (Reloaded) config initialized +[12:47:31] [main/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_216202_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagOrElementLocationMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:32] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: ModernFix reached bootstrap stage (9.773 s after launch) +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByName:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByValue:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getNeighborPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getAllFlags from me.jellysquid.mods.lithium.mixin.util.block_tracking.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_6104_ in embeddium.mixins.json:features.options.render_layers.LeavesBlockMixin, previously written by me.srrapero720.embeddiumplus.mixins.impl.leaves_culling.LeavesBlockMixin. Skipping method. +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:33] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: Vanilla bootstrap took 779 milliseconds +[12:47:34] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_47505_ in lithium.mixins.json:world.temperature_cache.BiomeMixin, previously written by org.embeddedt.modernfix.common.mixin.perf.remove_biome_temperature_cache.BiomeMixin. Skipping method. +[12:47:34] [pool-4-thread-1/INFO] [co.al.me.MERequester/]: Registering content +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/assets/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/data/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/INFO] [mojang/YggdrasilAuthenticationService]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' +[12:47:35] [Render thread/INFO] [minecraft/Minecraft]: Setting user: Ryexandrite +[12:47:35] [Render thread/INFO] [ModernFix/]: Bypassed Mojang DFU +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant is searching for constants in method with descriptor (Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/GuiMessageTag;)V +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 0 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\r, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 1 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\r +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = +, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 2 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn + +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\n, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 3 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\n +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found CLASS constant: value = Ljava/lang/String;, typeValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [{}] [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 4 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [{}] [CHAT] {} +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 5 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [CHAT] {} +[12:47:35] [Render thread/INFO] [defaultoptions/]: Loaded default options for extra-folder +[12:47:35] [Render thread/INFO] [ModernFix/]: Instantiating Mojang DFU +[12:47:36] [Render thread/INFO] [minecraft/Minecraft]: Backend library: LWJGL version 3.3.1 build 7 +[12:47:36] [Render thread/INFO] [KubeJS/]: Loaded client.properties +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Vendor: AMD +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Renderer: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Version: 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/WARN] [Embeddium++/Config]: Loading Embeddium++Config +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Updating config cache +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Cache updated successfully +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Initializing ImmediatelyFast 1.2.18+1.20.4 on AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (AMD) with OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: AMD GPU detected. Enabling coherent buffer mapping +[12:47:36] [Datafixer Bootstrap/INFO] [mojang/DataFixerBuilder]: 188 Datafixer optimizations took 85 milliseconds +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Found Iris/Oculus 1.7.0. Enabling compatibility. +[12:47:36] [Render thread/INFO] [Oculus/]: Debug functionality is disabled. +[12:47:36] [Render thread/INFO] [Oculus/]: OpenGL 4.5 detected, enabling DSA. +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled because no valid shaderpack is selected +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled +[12:47:36] [modloading-worker-0/INFO] [dynamiclightsreforged/]: [LambDynLights] Initializing Dynamic Lights Reforged... +[12:47:36] [modloading-worker-0/INFO] [LowDragLib/]: LowDragLib is initializing on platform: Forge +[12:47:36] [modloading-worker-0/INFO] [in.u_.u_.ut.ve.JarSignVerifier/]: Mod uteamcore is signed with a valid certificate. +[12:47:36] [modloading-worker-0/INFO] [de.ke.me.Melody/]: [MELODY] Loading Melody background audio library.. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_109501_ in embeddium.mixins.json:core.render.world.WorldRendererMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Successfully initialized! +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Server-side libs ready to use! +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_215924_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagEntryMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: Attempting to manually load Additional Placements config early. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: manual config load successful. +[12:47:36] [modloading-worker-0/WARN] [Additional Placements/]: During block registration you may recieve several reports of "Potentially Dangerous alternative prefix `additionalplacements`". Ignore these, they are intended. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [noisium/]: Loading Noisium. +[12:47:36] [modloading-worker-0/INFO] [co.cu.Cupboard/]: Loaded config for: cupboard.json +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:container_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:tile_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:container_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_data_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_nbt +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftblibrary:edit_nbt_response +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:sync_known_server_registries +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_config +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "craftpresence" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json" +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "unilib" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json" +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbessentials:update_tab_name +[12:47:37] [modloading-worker-0/INFO] [invtweaks/]: Registered 2 network packets +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded common.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded dev.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Looking for KubeJS plugins... +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_teams +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_message_history +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: GregTechCEu is initializing on platform: Forge +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:open_gui +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Configuration settings have been saved and reloaded successfully! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:open_my_team_gui +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:update_settings +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.latvian.mods.kubejs.integration.forge.gamestages.GameStagesIntegration does not have required mod gamestages loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ldlib +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source exposure +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source tfg +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_settings_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:send_message +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ftbxmodcompat +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:send_message_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_presence +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:create_party +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:player_gui_operation +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.ftb.mods.ftbxmodcompat.ftbchunks.kubejs.FTBChunksKubeJSPlugin does not have required mod ftbchunks loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [de.ke.dr.DrippyLoadingScreen/]: [DRIPPY LOADING SCREEN] Loading v3.0.1 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source lootjs +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source cucumber +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gtceu +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Initializing TerraFirmaCraft +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Options: Assertions Enabled = false, Boostrap = false, Test = false, Debug Logging = true +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Checking Discord for available assets with Client Id: 1182610212121743470 +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Originally coded by paulhobbel - https://github.com/paulhobbel +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.ForgeMod/FORGEMOD]: Forge mod loading, version 47.2.6, for MC 1.20.1 with MCP 20230612.114412 +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.MinecraftForge/FORGE]: MinecraftForge v47.2.6 Initialized +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gcyr +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_tfc +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.precpros.PrecProsPlugin does not have required mod precisionprospecting loaded, skipping +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.afc.AFCPlugin does not have required mod afc loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_create +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. +[12:47:37] [modloading-worker-0/INFO] [Every Compat/]: Loaded EveryCompat Create Module +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gcyr config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: High-Tier is Disabled. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Using default implementation for ThreadExecutor +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Done in 309.0 ms +[12:47:37] [CraftPresence/INFO] [craftpresence/]: 3 total assets detected! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_quests +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Booting... (platform: Forge, manual: false) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Found Mixin library. (version: 0.8.5) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Ready. As always, this mod will speed up your world loading and might or might not break it. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_team_data +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.SchedulerSignalerImpl/]: Initialized Scheduler Signaller of type: class net.creeperhost.ftbbackups.repack.org.quartz.core.SchedulerSignalerImpl +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Quartz Scheduler v.2.0.2 created. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_task_progress +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.si.RAMJobStore/]: RAMJobStore initialized. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:submit_task +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler meta-data: Quartz Scheduler (v2.0.2) 'ftbbackups2' with instanceId 'NON_CLUSTERED' + Scheduler class: 'net.creeperhost.ftbbackups.repack.org.quartz.core.QuartzScheduler' - running locally. + NOT STARTED. + Currently in standby mode. + Number of jobs executed: 0 + Using thread pool 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.SimpleThreadPool' - with 1 threads. + Using job-store 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. + +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler 'ftbbackups2' initialized from an externally provided properties instance. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler version: 2.0.2 +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:claim_reward_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:get_emergency_items +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_other_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_all_rewards +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_choice_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_completion_toast +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler ftbbackups2_$_NON_CLUSTERED started. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_item_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_chapter_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_chapter_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:force_save +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:set_custom_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_lock +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:reset_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:team_data_changed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:task_screen_config_req +[12:47:37] [modloading-worker-0/INFO] [co.jo.fl.ba.Backend/]: Oculus detected. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:task_screen_config_resp +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_progress +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_task_at +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:delete_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:delete_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:edit_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:edit_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_quest_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:change_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_reward_blocking +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_chapter_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:sync_structures_request +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_structures_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:request_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editor_permission +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:open_quest_book +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:clear_display_cache +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager... +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager took 11.32 ms +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised items. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbfiltersystem:sync_filter +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised blocks. +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised block entities. +[12:47:37] [modloading-worker-0/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Loading v3.2.3 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loading config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Built config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loaded config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled KubeJS integration +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Starting... +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Loading... +[12:47:37] [modloading-worker-0/INFO] [de.to.pa.PacketFixer/]: Packet Fixer has been initialized successfully +[12:47:37] [modloading-worker-0/INFO] [YetAnotherConfigLib/]: Deserializing YACLConfig from '/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/yacl.json5' +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] All done! +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading Rhino Minecraft remapper... +[12:47:37] [modloading-worker-0/INFO] [de.la.mo.rh.mo.ut.RhinoProperties/]: Rhino properties loaded. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading mappings for 1.20.1 +[12:47:37] [modloading-worker-0/WARN] [mixin/]: @Inject(@At("INVOKE")) Shift.BY=2 on create_connected.mixins.json:sequencedgearshift.SequencedGearshiftScreenMixin::handler$cfa000$updateParamsOfRow exceeds the maximum allowed value: 0. Increase the value of maxShiftBy to suppress this warning. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Done in 0.090 s +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registered bogey styles from railways +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering data fixers +[12:47:38] [modloading-worker-0/WARN] [Railways/]: Skipping Datafixer Registration due to it being disabled in the config. +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Hex Casting +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Oh The Biomes You'll Go +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Blue Skies +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Twilight Forest +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Biomes O' Plenty +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Nature's Spirit +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Dreams and Desires +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Quark +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for TerraFirmaCraft +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:main_startup_script.js in 0.058 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfc/constants.js in 0.024 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:horornot/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:minecraft/constants.js in 0.004 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:railways/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/machines.js in 0.008 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/material_info.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/recipe_types.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:framedblocks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmalife/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:sophisticated_backpacks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:more_red/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:mega_cells/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create/constants.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ae2/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create_additions/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:chisel_and_bits/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:extended_ae2/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:asticor_carts/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ftb_quests/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/fluids.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/materials.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:computer_craft/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded 30/30 KubeJS startup scripts in 0.721 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: example.js#3: TerraFirmaGreg the best modpack in the world :) +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:example.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:tooltips.js in 0.003 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded 2/2 KubeJS client scripts in 0.022 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#48: Loaded Java class 'net.minecraft.world.level.block.AmethystClusterBlock' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#49: Loaded Java class 'net.minecraft.world.level.block.Blocks' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#50: Loaded Java class 'net.minecraft.world.level.block.state.BlockBehaviour$Properties' +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:send_data_from_client +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:send_data_from_server +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:paint +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:add_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:remove_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:sync_stages +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:first_click +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:toast +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:reload_startup_scripts +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_server_errors +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_client_errors +[12:47:38] [Render thread/INFO] [GregTechCEu/]: GTCEu common proxy init! +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering material registries +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering GTCEu Materials +[12:47:38] [CraftPresence/INFO] [craftpresence/]: Attempting to connect to Discord (1/10)... +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering addon Materials +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:water +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:lava +[12:47:39] [CraftPresence/INFO] [craftpresence/]: Loaded display data with Client Id: 1182610212121743470 (Logged in as RyRy) +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering KeyBinds +[12:47:39] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'gtceu' took 1.043 s to run a deferred task. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@67141ef8 has been registered twice for the same name ae2:export_card. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@f4864e9 has been registered twice for the same name ae2:insert_card. +[12:47:42] [Render thread/INFO] [Moonlight/]: Initialized block sets in 21ms +[12:47:42] [Render thread/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ae2wtlib:cycle_terminal +[12:47:43] [Render thread/INFO] [Every Compat/]: Registering Compat WoodType Blocks +[12:47:43] [Render thread/INFO] [Every Compat/]: EveryCompat Create Module: registered 42 WoodType blocks +[12:47:43] [Render thread/INFO] [tf.TFCTumbleweed/]: Injecting TFC Tumbleweed override pack +[12:47:43] [Render thread/INFO] [co.ee.fi.FirmaLife/]: Injecting firmalife override pack +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:43] [Render thread/INFO] [Oculus/]: Hardware information: +[12:47:43] [Render thread/INFO] [Oculus/]: CPU: 16x AMD Ryzen 7 3700X 8-Core Processor +[12:47:43] [Render thread/INFO] [Oculus/]: GPU: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (Supports OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f)) +[12:47:43] [Render thread/INFO] [Oculus/]: OS: Linux (6.6.85) +[12:47:44] [Render thread/WARN] [mixin/]: Method overwrite conflict for isHidden in mixins.oculus.compat.sodium.json:copyEntity.ModelPartMixin, previously written by dev.tr7zw.firstperson.mixins.ModelPartMixin. Skipping method. +[12:47:44] [Render thread/INFO] [minecraft/Minecraft]: [FANCYMENU] Registering resource reload listener.. +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.ScreenCustomization/]: [FANCYMENU] Initializing screen customization engine! Addons should NOT REGISTER TO REGISTRIES anymore now! +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: STARTING +[12:47:44] [Render thread/INFO] [ModernFix/]: Invalidating pack caches +[12:47:44] [Render thread/INFO] [minecraft/ReloadableResourceManager]: Reloading ResourceManager: Additional Placements blockstate redirection pack, vanilla, mod_resources, gtceu:dynamic_assets, Moonlight Mods Dynamic Assets, Firmalife-1.20.1-2.1.15.jar:overload, TFCTumbleweed-1.20.1-1.2.2.jar:overload, KubeJS Resource Pack [assets], ldlib +[12:47:44] [Finalizer/WARN] [ModernFix/]: One or more BufferBuilders have been leaked, ModernFix will attempt to correct this. +[12:47:45] [Render thread/INFO] [Every Compat/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (everycomp) in: 597 ms +[12:47:45] [Render thread/INFO] [Moonlight/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (moonlight) in: 0 ms +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for pickupnotifier +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for hangglider +[12:47:45] [Worker-ResourceReload-4/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:45] [Worker-ResourceReload-3/INFO] [xa.pa.OpenPartiesAndClaims/]: Loading Open Parties and Claims! +[12:47:45] [Worker-ResourceReload-1/INFO] [co.re.RecipeEssentials/]: recipeessentials mod initialized +[12:47:45] [Worker-ResourceReload-10/INFO] [ne.dr.tf.TerraFirmaCraft/]: TFC Common Setup +[12:47:45] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [saturn] Starting version check at https://github.com/AbdElAziz333/Saturn/raw/mc1.20.1/dev/updates.json +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting game stages provider implementation to: KubeJS Stages +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [KubeJS Stages] as the active game stages implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting permissions provider implementation to: FTB Ranks +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [FTB Ranks] as the active permissions implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] recipe helper provider is [JEI] +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled FTB Filter System integration +[12:47:45] [Render thread/INFO] [GregTechCEu/]: GregTech Model loading took 520ms +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Initializing fonts for text rendering.. +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Calculating animation sizes for FancyMenu.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: drippy_loading_overlay +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Preloading animations! This could cause the loading screen to freeze for a while.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Finished preloading animations! +[12:47:46] [Render thread/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Starting late client initialization phase.. +[12:47:46] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:46] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Starting version check at https://updates.blamejared.com/get?n=controlling&gv=1.20.1 +[12:47:46] [Worker-ResourceReload-6/ERROR] [minecraft/SimpleJsonResourceReloadListener]: Couldn't parse data file tfc:field_guide/ru_ru/entries/tfg_ores/surface_copper from tfc:patchouli_books/field_guide/ru_ru/entries/tfg_ores/surface_copper.json +com.google.gson.JsonParseException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:526) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_263475_(GsonHelper.java:531) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_13776_(GsonHelper.java:581) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_278771_(SimpleJsonResourceReloadListener.java:41) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:29) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:17) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimplePreparableReloadListener.m_10786_(SimplePreparableReloadListener.java:11) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B,pl:mixin:APP:moonlight.mixins.json:ConditionHackMixin,pl:mixin:A} + at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?] {} + at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1657) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1463) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:569) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:422) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:779) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:725) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$34$1.read(TypeAdapters.java:1007) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:524) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + ... 13 more +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Render thread/INFO] [KubeJS Client/]: Client resource reload complete! +[12:47:46] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Worker-Main-6/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Found status: BETA Current: 12.0.2 Target: 12.0.2 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Starting version check at https://api.u-team.info/update/uteamcore.json +[12:47:47] [FTB Backups Config Watcher 0/INFO] [ne.cr.ft.FTBBackups/]: Config at /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/ftbbackups2.json has changed, reloaded! +[12:47:47] [Worker-ResourceReload-14/WARN] [minecraft/SpriteLoader]: Texture create_connected:block/fluid_container_window_debug with size 40x32 limits mip level from 4 to 3 +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "unilib" -> Outdated (Target version: "v1.0.5") +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "craftpresence" -> Outdated (Target version: "v2.5.4") +[12:47:47] [Render thread/INFO] [Every Compat/]: Registered 42 compat blocks making up 0.31% of total blocks registered +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Found status: OUTDATED Current: 5.1.4.312 Target: 5.1.4.346 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/pickupnotifier.json +[12:47:47] [Render thread/INFO] [Moonlight/]: Initialized color sets in 104ms +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: KubeJS TFC configuration: +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: Debug mode enabled: false +[12:47:47] [Render thread/INFO] [MEGA Cells/]: Initialised AE2WT integration. +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Found status: UP_TO_DATE Current: 8.0.0 Target: null +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Starting version check at https://update.maxhenkel.de/forge/corpse +[12:47:47] [Worker-ResourceReload-0/INFO] [FirstPersonModel/]: Loading FirstPerson Mod +[12:47:47] [Worker-ResourceReload-4/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 1/2 +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loading patreon trails data... +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loading patreon wing data... +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Client-side libs ready to use! +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loaded 45 patreon trails. +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loaded 21 patreon wings. +[12:47:47] [Worker-ResourceReload-7/INFO] [EMI/]: [EMI] Discovered Sodium +[12:47:47] [Worker-ResourceReload-14/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 1/2 +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_wut +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_restock +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:restock_amounts +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Found status: OUTDATED Current: 1.20.1-1.0.19 Target: 1.20.1-1.0.20 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Starting version check at https://raw.githubusercontent.com/hlysine/create_connected/main/update.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Found status: OUTDATED Current: 0.8.2-mc1.20.1 Target: 1.0.1-mc1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Starting version check at https://api.modrinth.com/updates/rubidium-extra/forge_updates.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Found status: AHEAD Current: 3.1.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/hangglider.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Found status: UP_TO_DATE Current: 8.0.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Starting version check at https://updates.blamejared.com/get?n=searchables&gv=1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Found status: BETA Current: 1.0.3 Target: 1.0.3 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Starting version check at https://api.modrinth.com/updates/cc-tweaked/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Found status: OUTDATED Current: 1.113.1 Target: 1.115.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Found status: AHEAD Current: 1.0.2 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Found status: AHEAD Current: 2.5.0 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Starting version check at https://api.modrinth.com/updates/radium/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Found status: OUTDATED Current: 0.12.3+git.50c5c33 Target: 0.12.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Starting version check at https://updates.blamejared.com/get?n=attributefix&gv=1.20.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Found status: BETA Current: 21.0.4 Target: 21.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Starting version check at https://updates.blamejared.com/get?n=clumps&gv=1.20.1 +[12:47:49] [Worker-ResourceReload-4/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking patreon: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Found status: BETA Current: 12.0.0.4 Target: 12.0.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [catalogue] Starting version check at https://mrcrayfish.com/modupdatejson?id=catalogue +[12:47:50] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzlesaccessapi.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Found status: BETA Current: 8.0.7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: tfcambiental:snowshoes#inventory +java.io.FileNotFoundException: tfcambiental:models/item/snowshoes.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:266) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119306_(ModelBakery.java:384) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:150) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: carpeted:block/label +java.io.FileNotFoundException: carpeted:models/block/label.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:262) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:159) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Found status: OUTDATED Current: 47.2.6 Target: 47.4.0 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Starting version check at https://raw.githubusercontent.com/MehVahdJukaar/Moonlight/multi-loader/forge/update.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Found status: BETA Current: 1.20-2.13.51 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Starting version check at https://raw.githubusercontent.com/Toma1O6/UpdateSchemas/master/configuration-forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Found status: OUTDATED Current: 2.2.0 Target: 2.2.1 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Starting version check at https://github.com/AbdElAziz333/SmoothBoot-Reloaded/raw/mc1.20.1/dev/updates.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Found status: UP_TO_DATE Current: 0.0.4 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Starting version check at https://raw.githubusercontent.com/VidTu/Ksyxis/main/updater_ksyxis_forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Found status: OUTDATED Current: 1.3.2 Target: 1.3.3 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Starting version check at https://api.modrinth.com/updates/flywheel/forge_updates.json +[12:47:50] [Worker-ResourceReload-4/ERROR] [xa.ma.WorldMap/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Found status: BETA Current: 0.6.10-7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Starting version check at https://raw.githubusercontent.com/DmitryLovin/pluginUpdate/master/invupdate.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Found status: UP_TO_DATE Current: 3.4.26 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzleslib.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Found status: OUTDATED Current: 8.1.23 Target: 8.1.32 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Starting version check at https://api.modrinth.com/updates/betterf3/forge_updates.json +[12:47:50] [Render thread/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 2/2 +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Found status: UP_TO_DATE Current: 7.0.2 Target: null +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Starting version check at https://api.modrinth.com/updates/packet-fixer/forge_updates.json +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Found status: OUTDATED Current: 1.4.2 Target: 2.0.0 +[12:47:51] [Render thread/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: World Map found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Iris found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 2/2 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: New world map region cache hash code: -815523079 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: map_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Xaero's minimap found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Iris found! +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model firmaciv:firmaciv_compass#inventory: + minecraft:textures/atlas/blocks.png:minecraft:item/compass +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:tin_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model createaddition:small_light_connector#facing=west,mode=push,powered=true,rotation=x_clockwise_180,variant=default: + minecraft:textures/atlas/blocks.png:create:block/chute_block +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:copper_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +Reloading Dynamic Lights +[12:47:57] [Render thread/INFO] [co.jo.fl.ba.Backend/]: Loaded all shader sources. +Create Crafts & Additions Initialized! +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded values for 19 compatible attributes. +[12:47:57] [Worker-ResourceReload-2/ERROR] [AttributeFix/]: Attribute ID 'minecolonies:mc_mob_damage' does not belong to a known attribute. This entry will be ignored. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded 20 values from config. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Saving config file. 20 entries. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Applying changes for 20 attributes. +[12:47:57] [Worker-ResourceReload-11/INFO] [de.me.as.AstikorCarts/]: Automatic pull animal configuration: +pull_animals = [ + "minecraft:camel", + "minecraft:donkey", + "minecraft:horse", + "minecraft:mule", + "minecraft:skeleton_horse", + "minecraft:zombie_horse", + "minecraft:player", + "tfc:donkey", + "tfc:mule", + "tfc:horse" + ] +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at io.github.mortuusars.exposure.integration.jade.ExposureJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at de.maxhenkel.corpse.integration.waila.PluginCorpse +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at xfacthd.framedblocks.common.compat.jade.FramedJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.general.GeneralPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.create.CreatePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at me.pandamods.fallingtrees.compat.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.gregtechceu.gtceu.integration.jade.GTJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at net.dries007.tfc.compat.jade.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.vanilla.VanillaPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.universal.UniversalPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.core.CorePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at appeng.integration.modules.jade.JadeModule +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.glodblock.github.extendedae.xmod.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at cy.jdkdigital.treetap.compat.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.eerussianguy.firmalife.compat.tooltip.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.ljuangbminecraft.tfcchannelcasting.compat.JadeIntegration +[12:47:59] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'create_connected' took 1.342 s to run a deferred task. +[12:47:59] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:59] [Render thread/INFO] [mojang/Library]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: Sound engine started +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: [FANCYMENU] Reloading AudioResourceHandler after Minecraft SoundEngine reload.. +[12:47:59] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 4096x2048x4 minecraft:textures/atlas/blocks.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x512x4 minecraft:textures/atlas/signs.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x1024x4 minecraft:textures/atlas/chest.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh particle. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_translucent. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_armor_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull_z_offset. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_smooth_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_decal. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_no_outline. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named IViewRotMat in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named FogShape in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light0_Direction in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light1_Direction in the specified shader program. +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x0 minecraft:textures/atlas/particles.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:48:00] [Render thread/INFO] [xa.ma.WorldMap/]: Successfully reloaded the world map shaders! +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Loading exposure filters: +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_blue_pane, exposure:shaders/light_blue_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:orange_pane, exposure:shaders/orange_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:red_pane, exposure:shaders/red_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:purple_pane, exposure:shaders/purple_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:blue_pane, exposure:shaders/blue_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_gray_pane, exposure:shaders/light_gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:magenta_pane, exposure:shaders/magenta_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:gray_pane, exposure:shaders/gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:lime_pane, exposure:shaders/lime_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:green_pane, exposure:shaders/green_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:pink_pane, exposure:shaders/pink_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:yellow_pane, exposure:shaders/yellow_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:white_pane, exposure:shaders/white_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:brown_pane, exposure:shaders/brown_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:glass_pane, exposure:shaders/crisp.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:interplanar_projector, exposure:shaders/invert.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:black_pane, exposure:shaders/black_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:cyan_pane, exposure:shaders/cyan_tint.json] added. +[12:48:00] [Render thread/INFO] [patchouli/]: BookContentResourceListenerLoader preloaded 1073 jsons +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x128x0 computercraft:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x0 polylib:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 jei:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 moonlight:textures/atlas/map_markers.png-atlas +[12:48:00] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Successfully reloaded the minimap shaders! +[12:48:00] [Render thread/INFO] [Shimmer/]: buildIn shimmer configuration is enabled, this can be disabled by config file +[12:48:00] [Render thread/INFO] [Shimmer/]: mod jar and resource pack discovery: file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.ResourceHandlers/]: [FANCYMENU] Reloading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.pr.ResourcePreLoader/]: [FANCYMENU] Pre-loading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Updating animation sizes.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: FINISHED +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: title_screen +[12:48:00] [Render thread/INFO] [Oculus/]: Creating pipeline for dimension NamespacedId{namespace='minecraft', name='overworld'} +[12:48:01] [Render thread/INFO] [ambientsounds/]: Loaded AmbientEngine 'basic' v3.1.0. 11 dimension(s), 11 features, 11 blockgroups, 2 sound collections, 37 regions, 58 sounds, 11 sound categories, 5 solids and 2 biome types +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: PlayerAnimator not found! +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Vanilla Hands items: [] +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Auto Disable items: [camera] +[12:48:02] [Render thread/WARN] [ModernFix/]: Game took 40.304 seconds to start diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log new file mode 100644 index 000000000..51e5ec546 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log @@ -0,0 +1,2854 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG + + , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Outdated (Target version: "v2.5.4")]]> + +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. + + + + + Outdated (Target version: "v1.0.5")]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (ModelBakery.java:150) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + (ModelBakery.java:159) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Reloading Dynamic Lights + + + +Create Crafts & Additions Initialized! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt new file mode 100644 index 000000000..65e1c569e --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt @@ -0,0 +1,945 @@ +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +UNKNOWN +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +UNKNOWN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +WARN +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +UNKNOWN +INFO +UNKNOWN +INFO +ERROR +INFO +INFO +INFO +INFO +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt new file mode 100644 index 000000000..08d5100a0 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt @@ -0,0 +1,869 @@ +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +UNKNOWN +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +UNKNOWN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +UNKNOWN +INFO +UNKNOWN +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +UNKNOWN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO diff --git a/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt new file mode 100644 index 000000000..02734e56f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt @@ -0,0 +1,25 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.text.log b/tests/testdata/TestLogs/vanilla-1.21.5.text.log new file mode 100644 index 000000000..e78702858 --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.text.log @@ -0,0 +1,25 @@ +[12:50:56] [Datafixer Bootstrap/INFO]: 263 Datafixer optimizations took 908 milliseconds +[12:50:58] [Render thread/INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD] +[12:50:58] [Render thread/INFO]: Setting user: Ryexandrite +[12:50:58] [Render thread/INFO]: Backend library: LWJGL version 3.3.3+5 +[12:50:58] [Render thread/INFO]: Using optional rendering extensions: GL_KHR_debug, GL_ARB_vertex_attrib_binding, GL_ARB_direct_state_access +[12:50:58] [Render thread/INFO]: Reloading ResourceManager: vanilla +[12:50:59] [Worker-Main-6/INFO]: Found unifont_all_no_pua-16.0.01.hex, loading +[12:50:59] [Worker-Main-7/INFO]: Found unifont_jp_patch-16.0.01.hex, loading +[12:50:59] [Render thread/WARN]: minecraft:pipeline/entity_translucent_emissive shader program does not use sampler Sampler2 defined in the pipeline. This might be a bug. +[12:50:59] [Render thread/INFO]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:50:59] [Render thread/INFO]: Sound engine started +[12:50:59] [Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas +[12:50:59] [Render thread/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:50:59] [Render thread/INFO]: Created: 64x64x0 minecraft:textures/atlas/map_decorations.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/particles.png-atlas +[12:51:00] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:51:00] [Render thread/INFO]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:51:00] [Render thread/INFO]: Created: 1024x512x0 minecraft:textures/atlas/gui.png-atlas diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.xml.log b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log new file mode 100644 index 000000000..24bfe0b7f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/ninjatracing.py b/tools/ninjatracing.py new file mode 100644 index 000000000..418124855 --- /dev/null +++ b/tools/ninjatracing.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# Copyright 2018 Nico Weber +# Patched to work with v7 logs +# +# 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. + +"""Converts one (or several) .ninja_log files into chrome's about:tracing format. + +If clang -ftime-trace .json files are found adjacent to generated files they +are embedded. + +Usage: + ninja -C $BUILDDIR + ninjatracing $BUILDDIR/.ninja_log > trace.json +""" + +import json +import os +import optparse +import re +import sys + + +class Target: + """Represents a single line read for a .ninja_log file. Start and end times + are milliseconds.""" + def __init__(self, start, end): + self.start = int(start) + self.end = int(end) + self.targets = [] + + +def read_targets(log, show_all): + """Reads all targets from .ninja_log file |log_file|, sorted by start + time""" + header = log.readline() + m = re.search(r'^# ninja log v(\d+)\n$', header) + assert m, "unrecognized ninja log version %r" % header + version = int(m.group(1)) + assert 5 <= version <= 7, "unsupported ninja log version %d" % version + if version >= 6: + # Skip header line + next(log) + + targets = {} + last_end_seen = 0 + for line in log: + if line.startswith('#'): + continue + start, end, _, name, cmdhash = line.strip().split('\t') # Ignore restat. + if not show_all and int(end) < last_end_seen: + # An earlier time stamp means that this step is the first in a new + # build, possibly an incremental build. Throw away the previous data + # so that this new build will be displayed independently. + targets = {} + last_end_seen = int(end) + targets.setdefault(cmdhash, Target(start, end)).targets.append(name) + return sorted(targets.values(), key=lambda job: job.end, reverse=True) + + +class Threads: + """Tries to reconstruct the parallelism from a .ninja_log""" + def __init__(self): + self.workers = [] # Maps thread id to time that thread is occupied for. + + def alloc(self, target): + """Places target in an available thread, or adds a new thread.""" + for worker in range(len(self.workers)): + if self.workers[worker] >= target.end: + self.workers[worker] = target.start + return worker + self.workers.append(target.start) + return len(self.workers) - 1 + + +def read_events(trace, options): + """Reads all events from time-trace json file |trace|.""" + trace_data = json.load(trace) + + def include_event(event, options): + """Only include events if they are complete events, are longer than + granularity, and are not totals.""" + return ((event['ph'] == 'X') and + (event['dur'] >= options['granularity']) and + (not event['name'].startswith('Total'))) + + return [x for x in trace_data['traceEvents'] if include_event(x, options)] + + +def trace_to_dicts(target, trace, options, pid, tid): + """Read a file-like object |trace| containing -ftime-trace data and yields + about:tracing dict per eligible event in that log.""" + ninja_time = (target.end - target.start) * 1000 + for event in read_events(trace, options): + # Check if any event duration is greater than the duration from ninja. + if event['dur'] > ninja_time: + print("Inconsistent timing found (clang time > ninja time). Please" + " ensure that timings are from consistent builds.") + sys.exit(1) + + # Set tid and pid from ninja log. + event['pid'] = pid + event['tid'] = tid + + # Offset trace time stamp by ninja start time. + event['ts'] += (target.start * 1000) + + yield event + + +def embed_time_trace(ninja_log_dir, target, pid, tid, options): + """Produce time trace output for the specified ninja target. Expects + time-trace file to be in .json file named based on .o file.""" + for t in target.targets: + o_path = os.path.join(ninja_log_dir, t) + json_trace_path = os.path.splitext(o_path)[0] + '.json' + try: + with open(json_trace_path, 'r') as trace: + for time_trace_event in trace_to_dicts(target, trace, options, + pid, tid): + yield time_trace_event + except IOError: + pass + + +def log_to_dicts(log, pid, options): + """Reads a file-like object |log| containing a .ninja_log, and yields one + about:tracing dict per command found in the log.""" + threads = Threads() + for target in read_targets(log, options['showall']): + tid = threads.alloc(target) + + yield { + 'name': '%0s' % ', '.join(target.targets), 'cat': 'targets', + 'ph': 'X', 'ts': (target.start * 1000), + 'dur': ((target.end - target.start) * 1000), + 'pid': pid, 'tid': tid, 'args': {}, + } + if options.get('embed_time_trace', False): + # Add time-trace information into the ninja trace. + try: + ninja_log_dir = os.path.dirname(log.name) + except AttributeError: + continue + for time_trace in embed_time_trace(ninja_log_dir, target, pid, + tid, options): + yield time_trace + + +def main(argv): + usage = __doc__ + parser = optparse.OptionParser(usage) + parser.add_option('-a', '--showall', action='store_true', dest='showall', + default=False, + help='report on last build step for all outputs. Default ' + 'is to report just on the last (possibly incremental) ' + 'build') + parser.add_option('-g', '--granularity', type='int', default=50000, + dest='granularity', + help='minimum length time-trace event to embed in ' + 'microseconds. Default: %default') + parser.add_option('-e', '--embed-time-trace', action='store_true', + default=False, dest='embed_time_trace', + help='embed clang -ftime-trace json file found adjacent ' + 'to a target file') + (options, args) = parser.parse_args() + + if len(args) == 0: + print('Must specify at least one .ninja_log file') + parser.print_help() + return 1 + + entries = [] + for pid, log_file in enumerate(args): + with open(log_file, 'r') as log: + entries += list(log_to_dicts(log, pid, vars(options))) + json.dump(entries, sys.stdout) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 000000000..20811632c --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,20 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ], + "overlay-ports": [ + "./cmake/vcpkg-ports" + ], + "overlay-triplets": [ + "./cmake/vcpkg-triplets" + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..942e6d9e4 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,31 @@ +{ + "dependencies": [ + { + "name": "ecm", + "host": true + }, + { + "name": "libqrencode", + "default-features": false + }, + { + "name": "pkgconf", + "host": true + }, + + "cmark", + { + "name": "libarchive", + "default-features": false, + "features": [ + "bzip2", + "lz4", + "lzma", + "lzo", + "zstd" + ] + }, + "tomlplusplus", + "zlib" + ] +}